From 9970cdff58c2ce77e7be78b116cd3e1bce5ce785 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 5 Jun 2025 09:53:53 +0200 Subject: [PATCH 1/3] perf: drop `babel` to reduce the server bundle size --- .changeset/new-wombats-crash.md | 5 + .../open-next/src/build/createServerBundle.ts | 25 +-- .../src/build/patch/patches/dropBabel.ts | 88 ++++++++ .../src/build/patch/patches/index.ts | 1 + .../build/patch/patches/dropBabel.test.ts | 194 ++++++++++++++++++ 5 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 .changeset/new-wombats-crash.md create mode 100644 packages/open-next/src/build/patch/patches/dropBabel.ts create mode 100644 packages/tests-unit/tests/build/patch/patches/dropBabel.test.ts diff --git a/.changeset/new-wombats-crash.md b/.changeset/new-wombats-crash.md new file mode 100644 index 000000000..e1b560a67 --- /dev/null +++ b/.changeset/new-wombats-crash.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +perf: drop `babel` to reduce the server bundle size diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 3582f5549..303c6b651 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -21,15 +21,7 @@ import { import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; -import { - patchBackgroundRevalidation, - patchEnvVars, - patchFetchCacheForISR, - patchFetchCacheSetMissingWaitUntil, - patchNextServer, - patchUnstableCacheForISR, - patchUseCacheForISR, -} from "./patch/patches/index.js"; +import * as patches from "./patch/patches/index.js"; interface CodeCustomization { // These patches are meant to apply on user and next generated code @@ -207,13 +199,14 @@ async function generateBundle( const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; await applyCodePatches(options, tracedFiles, manifests, [ - patchFetchCacheSetMissingWaitUntil, - patchFetchCacheForISR, - patchUnstableCacheForISR, - patchNextServer, - patchEnvVars, - patchBackgroundRevalidation, - patchUseCacheForISR, + patches.patchFetchCacheSetMissingWaitUntil, + patches.patchFetchCacheForISR, + patches.patchUnstableCacheForISR, + patches.patchNextServer, + patches.patchEnvVars, + patches.patchBackgroundRevalidation, + patches.patchUseCacheForISR, + patches.patchDropBabel, ...additionalCodePatches, ]); diff --git a/packages/open-next/src/build/patch/patches/dropBabel.ts b/packages/open-next/src/build/patch/patches/dropBabel.ts new file mode 100644 index 000000000..14e23a7d2 --- /dev/null +++ b/packages/open-next/src/build/patch/patches/dropBabel.ts @@ -0,0 +1,88 @@ +/** + * Patches to avoid pulling babel (~4MB). + * + * Details: + * - empty `NextServer#runMiddleware` and `NextServer#runEdgeFunction` that are not used + * - drop `next/dist/server/node-environment-extensions/error-inspect.js` + */ + +import { getCrossPlatformPathRegex } from "utils/regex"; +import { patchCode } from "../astCodePatcher"; +import type { CodePatcher } from "../codePatcher"; + +export const patchDropBabel: CodePatcher = { + name: "patch-drop-babel", + patches: [ + // Empty the body of `NextServer#runMiddleware` + { + field: { + pathFilter: getCrossPlatformPathRegex( + String.raw`/next/dist/server/next-server\.js$`, + { + escape: false, + }, + ), + contentFilter: /runMiddleware\(/, + patchCode: async ({ code }) => + patchCode(code, createEmptyBodyRule("runMiddleware")), + }, + }, + // Empty the body of `NextServer#runEdgeFunction` + { + field: { + pathFilter: getCrossPlatformPathRegex( + String.raw`/next/dist/server/next-server\.js$`, + { + escape: false, + }, + ), + contentFilter: /runMiddleware\(/, + patchCode: async ({ code }) => + patchCode(code, createEmptyBodyRule("runEdgeFunction")), + }, + }, + // Drop `error-inspect` that pulls babel + { + field: { + pathFilter: getCrossPlatformPathRegex( + String.raw`next/dist/server/node-environment\.js$`, + { + escape: false, + }, + ), + contentFilter: /error-inspect/, + patchCode: async ({ code }) => patchCode(code, "errorInspectRule"), + }, + }, + ], +}; + +/** + * Swaps the body for a throwing implementation + * + * @param methodName The name of the method + * @returns A rule to replace the body with a `throw` + */ +export function createEmptyBodyRule(methodName: string) { + return ` +rule: + pattern: + selector: method_definition + context: "class { async ${methodName}($$$PARAMS) { $$$_ } }" +fix: |- + async ${methodName}($$$PARAMS) { + throw new Error("${methodName} should not be called with OpenNext"); + } +`; +} + +/** + * Drops `require("./node-environment-extensions/error-inspect");` + */ +export const errorInspectRule = ` +rule: + pattern: require("./node-environment-extensions/error-inspect"); +fix: |- + // Removed by OpenNext + // require("./node-environment-extensions/error-inspect"); +`; diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts index bd46d6532..bd7ec945e 100644 --- a/packages/open-next/src/build/patch/patches/index.ts +++ b/packages/open-next/src/build/patch/patches/index.ts @@ -7,3 +7,4 @@ export { } from "./patchFetchCacheISR.js"; export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js"; export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js"; +export { patchDropBabel } from "./dropBabel.js"; diff --git a/packages/tests-unit/tests/build/patch/patches/dropBabel.test.ts b/packages/tests-unit/tests/build/patch/patches/dropBabel.test.ts new file mode 100644 index 000000000..63af54789 --- /dev/null +++ b/packages/tests-unit/tests/build/patch/patches/dropBabel.test.ts @@ -0,0 +1,194 @@ +import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { + createEmptyBodyRule, + errorInspectRule, +} from "@opennextjs/aws/build/patch/patches/dropBabel.js"; +import { describe, expect, test } from "vitest"; + +describe("babel-drop", () => { + test("Drop body", () => { + const code = ` +class NextNodeServer extends _baseserver.default { + constructor(options){ + // Initialize super class + super(options); + this.handleNextImageRequest = async (req, res, parsedUrl) => { /* ... */ }; + } + async handleUpgrade() { + // The web server does not support web sockets, it's only used for HMR in + // development. + } + getEnabledDirectories(dev) { + const dir = dev ? this.dir : this.serverDistDir; + return { + app: (0, _findpagesdir.findDir)(dir, "app") ? true : false, + pages: (0, _findpagesdir.findDir)(dir, "pages") ? true : false + }; + } + /** + * This method gets all middleware matchers and execute them when the request + * matches. It will make sure that each middleware exists and is compiled and + * ready to be invoked. The development server will decorate it to add warns + * and errors with rich traces. + */ async runMiddleware(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('invariant: runMiddleware should not be called in minimal mode'); + } + // Middleware is skipped for on-demand revalidate requests + if ((0, _apiutils.checkIsOnDemandRevalidate)(params.request, this.renderOpts.previewProps).isOnDemandRevalidate) { + return { + response: new Response(null, { + headers: { + 'x-middleware-next': '1' + } + }) + }; + } + // ... + } + async runEdgeFunction(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('Middleware is not supported in minimal mode.'); + } + let edgeInfo; + const { query, page, match } = params; + if (!match) await this.ensureEdgeFunction({ + page, + appPaths: params.appPaths, + url: params.req.url + }); + // ... + } + // ... +}`; + + expect( + patchCode(code, createEmptyBodyRule("runMiddleware")), + ).toMatchInlineSnapshot(` + "class NextNodeServer extends _baseserver.default { + constructor(options){ + // Initialize super class + super(options); + this.handleNextImageRequest = async (req, res, parsedUrl) => { /* ... */ }; + } + async handleUpgrade() { + // The web server does not support web sockets, it's only used for HMR in + // development. + } + getEnabledDirectories(dev) { + const dir = dev ? this.dir : this.serverDistDir; + return { + app: (0, _findpagesdir.findDir)(dir, "app") ? true : false, + pages: (0, _findpagesdir.findDir)(dir, "pages") ? true : false + }; + } + /** + * This method gets all middleware matchers and execute them when the request + * matches. It will make sure that each middleware exists and is compiled and + * ready to be invoked. The development server will decorate it to add warns + * and errors with rich traces. + */ async runMiddleware(params) { + throw new Error("runMiddleware should not be called with OpenNext"); + } + async runEdgeFunction(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('Middleware is not supported in minimal mode.'); + } + let edgeInfo; + const { query, page, match } = params; + if (!match) await this.ensureEdgeFunction({ + page, + appPaths: params.appPaths, + url: params.req.url + }); + // ... + } + // ... + }" + `); + + expect( + patchCode(code, createEmptyBodyRule("runEdgeFunction")), + ).toMatchInlineSnapshot(` + "class NextNodeServer extends _baseserver.default { + constructor(options){ + // Initialize super class + super(options); + this.handleNextImageRequest = async (req, res, parsedUrl) => { /* ... */ }; + } + async handleUpgrade() { + // The web server does not support web sockets, it's only used for HMR in + // development. + } + getEnabledDirectories(dev) { + const dir = dev ? this.dir : this.serverDistDir; + return { + app: (0, _findpagesdir.findDir)(dir, "app") ? true : false, + pages: (0, _findpagesdir.findDir)(dir, "pages") ? true : false + }; + } + /** + * This method gets all middleware matchers and execute them when the request + * matches. It will make sure that each middleware exists and is compiled and + * ready to be invoked. The development server will decorate it to add warns + * and errors with rich traces. + */ async runMiddleware(params) { + if (process.env.NEXT_MINIMAL) { + throw new Error('invariant: runMiddleware should not be called in minimal mode'); + } + // Middleware is skipped for on-demand revalidate requests + if ((0, _apiutils.checkIsOnDemandRevalidate)(params.request, this.renderOpts.previewProps).isOnDemandRevalidate) { + return { + response: new Response(null, { + headers: { + 'x-middleware-next': '1' + } + }) + }; + } + // ... + } + async runEdgeFunction(params) { + throw new Error("runEdgeFunction should not be called with OpenNext"); + } + // ... + }" + `); + }); + + test("Error Inspect", () => { + const code = ` +// This file should be imported before any others. It sets up the environment +// for later imports to work properly. +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +require("./node-environment-baseline"); +require("./node-environment-extensions/error-inspect"); +require("./node-environment-extensions/random"); +require("./node-environment-extensions/date"); +require("./node-environment-extensions/web-crypto"); +require("./node-environment-extensions/node-crypto"); +//# sourceMappingURL=node-environment.js.map +}`; + + expect(patchCode(code, errorInspectRule)).toMatchInlineSnapshot(` + "// This file should be imported before any others. It sets up the environment + // for later imports to work properly. + "use strict"; + Object.defineProperty(exports, "__esModule", { + value: true + }); + require("./node-environment-baseline"); + // Removed by OpenNext + // require("./node-environment-extensions/error-inspect"); + require("./node-environment-extensions/random"); + require("./node-environment-extensions/date"); + require("./node-environment-extensions/web-crypto"); + require("./node-environment-extensions/node-crypto"); + //# sourceMappingURL=node-environment.js.map + }" + `); + }); +}); From 07c3ecf6141808d5b14cc9b0899a8630769c5640 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 5 Jun 2025 10:10:24 +0200 Subject: [PATCH 2/3] fixup: thanks copilot! --- packages/open-next/src/build/patch/patches/dropBabel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/open-next/src/build/patch/patches/dropBabel.ts b/packages/open-next/src/build/patch/patches/dropBabel.ts index 14e23a7d2..780aec02b 100644 --- a/packages/open-next/src/build/patch/patches/dropBabel.ts +++ b/packages/open-next/src/build/patch/patches/dropBabel.ts @@ -36,7 +36,7 @@ export const patchDropBabel: CodePatcher = { escape: false, }, ), - contentFilter: /runMiddleware\(/, + contentFilter: /runEdgeFunction\(/, patchCode: async ({ code }) => patchCode(code, createEmptyBodyRule("runEdgeFunction")), }, @@ -51,7 +51,7 @@ export const patchDropBabel: CodePatcher = { }, ), contentFilter: /error-inspect/, - patchCode: async ({ code }) => patchCode(code, "errorInspectRule"), + patchCode: async ({ code }) => patchCode(code, errorInspectRule), }, }, ], From cf9d80693cd378052c385ddf3728ff06e3f4fd96 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 5 Jun 2025 10:20:09 +0200 Subject: [PATCH 3/3] fixup! add .js --- packages/open-next/src/build/patch/patches/dropBabel.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/open-next/src/build/patch/patches/dropBabel.ts b/packages/open-next/src/build/patch/patches/dropBabel.ts index 780aec02b..7e2eae519 100644 --- a/packages/open-next/src/build/patch/patches/dropBabel.ts +++ b/packages/open-next/src/build/patch/patches/dropBabel.ts @@ -6,9 +6,9 @@ * - drop `next/dist/server/node-environment-extensions/error-inspect.js` */ -import { getCrossPlatformPathRegex } from "utils/regex"; -import { patchCode } from "../astCodePatcher"; -import type { CodePatcher } from "../codePatcher"; +import { getCrossPlatformPathRegex } from "utils/regex.js"; +import { patchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; export const patchDropBabel: CodePatcher = { name: "patch-drop-babel",