Skip to content

Commit e9b37fd

Browse files
authoredApr 7, 2025
Improve Perf (#800)
* remove useless call and import * add test for next server * refactor * add env var test & move test * lint * changeset * handle preloading * review * added some comment
1 parent a1992c9 commit e9b37fd

17 files changed

+1607
-10
lines changed
 

‎.changeset/warm-pans-argue.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
Some perf improvements :
6+
- Eliminate unnecessary runtime imports (i.e. dev react dependencies and next precompiled dev or turbopack dependencies)
7+
- Refactor route preloading to be either on-demand or using waitUntil or at the start or during warmerEvent.
8+
- Add a global function to preload routes when needed.

‎packages/open-next/src/build/createServerBundle.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ import * as buildHelper from "./helper.js";
2222
import { installDependencies } from "./installDeps.js";
2323
import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js";
2424
import {
25+
patchEnvVars,
2526
patchFetchCacheForISR,
27+
patchFetchCacheSetMissingWaitUntil,
28+
patchNextServer,
2629
patchUnstableCacheForISR,
27-
} from "./patch/patchFetchCacheISR.js";
28-
import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.js";
30+
} from "./patch/patches/index.js";
2931

3032
interface CodeCustomization {
3133
// These patches are meant to apply on user and next generated code
@@ -200,6 +202,8 @@ async function generateBundle(
200202
patchFetchCacheSetMissingWaitUntil,
201203
patchFetchCacheForISR,
202204
patchUnstableCacheForISR,
205+
patchNextServer,
206+
patchEnvVars,
203207
...additionalCodePatches,
204208
]);
205209

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2+
export { patchNextServer } from "./patchNextServer.js";
3+
export {
4+
patchFetchCacheForISR,
5+
patchUnstableCacheForISR,
6+
} from "./patchFetchCacheISR.js";
7+
export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createPatchCode } from "../astCodePatcher.js";
2+
import type { CodePatcher } from "../codePatcher";
3+
4+
/**
5+
* Creates a rule to replace `process.env.${envVar}` by `value` in the condition of if statements
6+
* This is used to avoid loading unnecessary deps at runtime
7+
* @param envVar The env var that we want to replace
8+
* @param value The value that we want to replace it with
9+
* @returns
10+
*/
11+
export const envVarRuleCreator = (envVar: string, value: string) => `
12+
rule:
13+
kind: member_expression
14+
pattern: process.env.${envVar}
15+
inside:
16+
kind: parenthesized_expression
17+
stopBy: end
18+
inside:
19+
kind: if_statement
20+
fix:
21+
'${value}'
22+
`;
23+
24+
export const patchEnvVars: CodePatcher = {
25+
name: "patch-env-vars",
26+
patches: [
27+
// This patch will set the `NEXT_RUNTIME` env var to "node" to avoid loading unnecessary edge deps at runtime
28+
{
29+
versions: ">=15.0.0",
30+
field: {
31+
pathFilter: /module\.compiled\.js$/,
32+
contentFilter: /process\.env\.NEXT_RUNTIME/,
33+
patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')),
34+
},
35+
},
36+
// This patch will set `NODE_ENV` to production to avoid loading unnecessary dev deps at runtime
37+
{
38+
versions: ">=15.0.0",
39+
field: {
40+
pathFilter:
41+
/(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/,
42+
contentFilter: /process\.env\.NODE_ENV/,
43+
patchCode: createPatchCode(
44+
envVarRuleCreator("NODE_ENV", '"production"'),
45+
),
46+
},
47+
},
48+
// This patch will set `TURBOPACK` env to false to avoid loading turbopack related deps at runtime
49+
{
50+
versions: ">=15.0.0",
51+
field: {
52+
pathFilter: /module\.compiled\.js$/,
53+
contentFilter: /process\.env\.TURBOPACK/,
54+
patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")),
55+
},
56+
},
57+
],
58+
};

‎packages/open-next/src/build/patch/patchFetchCacheISR.ts renamed to ‎packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Lang } from "@ast-grep/napi";
22
import { getCrossPlatformPathRegex } from "utils/regex.js";
3-
import { createPatchCode } from "./astCodePatcher.js";
4-
import type { CodePatcher } from "./codePatcher";
3+
import { createPatchCode } from "../astCodePatcher.js";
4+
import type { CodePatcher } from "../codePatcher.js";
55

66
export const fetchRule = `
77
rule:

‎packages/open-next/src/build/patch/patchFetchCacheWaitUntil.ts renamed to ‎packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getCrossPlatformPathRegex } from "utils/regex.js";
2-
import { createPatchCode } from "./astCodePatcher.js";
3-
import type { CodePatcher } from "./codePatcher";
2+
import { createPatchCode } from "../astCodePatcher.js";
3+
import type { CodePatcher } from "../codePatcher.js";
44

55
export const rule = `
66
rule:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createPatchCode } from "../astCodePatcher.js";
2+
import type { CodePatcher } from "../codePatcher.js";
3+
4+
// This rule will replace the `NEXT_MINIMAL` env variable with true in multiple places to avoid executing unwanted path (i.e. next middleware, edge functions and image optimization)
5+
export const minimalRule = `
6+
rule:
7+
kind: member_expression
8+
pattern: process.env.NEXT_MINIMAL
9+
any:
10+
- inside:
11+
kind: parenthesized_expression
12+
stopBy: end
13+
inside:
14+
kind: if_statement
15+
any:
16+
- inside:
17+
kind: statement_block
18+
inside:
19+
kind: method_definition
20+
any:
21+
- has: {kind: property_identifier, field: name, regex: runEdgeFunction}
22+
- has: {kind: property_identifier, field: name, regex: runMiddleware}
23+
- has: {kind: property_identifier, field: name, regex: imageOptimizer}
24+
- has:
25+
kind: statement_block
26+
has:
27+
kind: expression_statement
28+
pattern: res.statusCode = 400;
29+
fix:
30+
'true'
31+
`;
32+
33+
// This rule will disable the background preloading of route done by NextServer by default during the creation of NextServer
34+
export const disablePreloadingRule = `
35+
rule:
36+
kind: statement_block
37+
inside:
38+
kind: if_statement
39+
any:
40+
- has:
41+
kind: member_expression
42+
pattern: this.nextConfig.experimental.preloadEntriesOnStart
43+
stopBy: end
44+
- has:
45+
kind: binary_expression
46+
pattern: appDocumentPreloading === true
47+
stopBy: end
48+
fix:
49+
'{}'
50+
`;
51+
52+
// This rule is mostly for splitted edge functions so that we don't try to match them on the other non edge functions
53+
export const removeMiddlewareManifestRule = `
54+
rule:
55+
kind: statement_block
56+
inside:
57+
kind: method_definition
58+
has:
59+
kind: property_identifier
60+
regex: getMiddlewareManifest
61+
fix:
62+
'{return null;}'
63+
`;
64+
65+
export const patchNextServer: CodePatcher = {
66+
name: "patch-next-server",
67+
patches: [
68+
// Skip executing next middleware, edge functions and image optimization inside NextServer
69+
{
70+
versions: ">=15.0.0",
71+
field: {
72+
pathFilter: /next-server\.(js)$/,
73+
contentFilter: /process\.env\.NEXT_MINIMAL/,
74+
patchCode: createPatchCode(minimalRule),
75+
},
76+
},
77+
// Disable Next background preloading done at creation of `NetxServer`
78+
{
79+
versions: ">=15.0.0",
80+
field: {
81+
pathFilter: /next-server\.(js)$/,
82+
contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/,
83+
patchCode: createPatchCode(disablePreloadingRule),
84+
},
85+
},
86+
// Don't match edge functions in `NextServer`
87+
{
88+
versions: ">=15.0.0",
89+
field: {
90+
pathFilter: /next-server\.(js)$/,
91+
contentFilter: /getMiddlewareManifest/,
92+
patchCode: createPatchCode(removeMiddlewareManifestRule),
93+
},
94+
},
95+
],
96+
};

‎packages/open-next/src/core/createMainHandler.ts

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export async function createMainHandler() {
2626
globalThis.serverId = generateUniqueId();
2727
globalThis.openNextConfig = config;
2828

29+
// If route preloading behavior is set to start, it will wait for every single route to be preloaded before even creating the main handler.
30+
await globalThis.__next_route_preloader("start");
31+
2932
// Default queue
3033
globalThis.queue = await resolveQueue(thisFunction.override?.queue);
3134

‎packages/open-next/src/core/requestHandler.ts

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export async function openNextHandler(
4747
waitUntil: options?.waitUntil,
4848
},
4949
async () => {
50+
await globalThis.__next_route_preloader("waitUntil");
5051
if (initialHeaders["x-forwarded-host"]) {
5152
initialHeaders.host = initialHeaders["x-forwarded-host"];
5253
}

‎packages/open-next/src/core/util.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
// @ts-ignore
77
import NextServer from "next/dist/server/next-server.js";
88

9-
import { debug } from "../adapters/logger.js";
9+
import { debug, error } from "../adapters/logger.js";
1010
import {
1111
applyOverride as applyNextjsRequireHooksOverride,
1212
overrideHooks as overrideNextjsRequireHooks,
@@ -59,6 +59,53 @@ const nextServer = new NextServer.default({
5959
dir: __dirname,
6060
});
6161

62+
let routesLoaded = false;
63+
64+
globalThis.__next_route_preloader = async (stage) => {
65+
if (routesLoaded) {
66+
return;
67+
}
68+
const thisFunction = globalThis.fnName
69+
? globalThis.openNextConfig.functions![globalThis.fnName]
70+
: globalThis.openNextConfig.default;
71+
const routePreloadingBehavior =
72+
thisFunction?.routePreloadingBehavior ?? "none";
73+
if (routePreloadingBehavior === "none") {
74+
routesLoaded = true;
75+
return;
76+
}
77+
if (!("unstable_preloadEntries" in nextServer)) {
78+
debug(
79+
"The current version of Next.js does not support route preloading. Skipping route preloading.",
80+
);
81+
routesLoaded = true;
82+
return;
83+
}
84+
if (stage === "waitUntil" && routePreloadingBehavior === "withWaitUntil") {
85+
// We need to access the waitUntil
86+
const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil;
87+
if (!waitUntil) {
88+
error(
89+
"You've tried to use the 'withWaitUntil' route preloading behavior, but the 'waitUntil' function is not available.",
90+
);
91+
routesLoaded = true;
92+
return;
93+
}
94+
debug("Preloading entries with waitUntil");
95+
waitUntil?.(nextServer.unstable_preloadEntries());
96+
routesLoaded = true;
97+
} else if (
98+
(stage === "start" && routePreloadingBehavior === "onStart") ||
99+
(stage === "warmerEvent" && routePreloadingBehavior === "onWarmerEvent") ||
100+
stage === "onDemand"
101+
) {
102+
const startTimestamp = Date.now();
103+
debug("Preloading entries");
104+
await nextServer.unstable_preloadEntries();
105+
debug("Preloading entries took", Date.now() - startTimestamp, "ms");
106+
routesLoaded = true;
107+
}
108+
};
62109
// `getRequestHandlerWithMetadata` is not available in older versions of Next.js
63110
// It is required to for next 15.2 to pass metadata for page router data route
64111
export const requestHandler = (metadata: Record<string, any>) =>

‎packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const handler: WrapperHandler = async (handler, converter) =>
3535
if ("type" in event) {
3636
const result = await formatWarmerResponse(event);
3737
responseStream.end(Buffer.from(JSON.stringify(result)), "utf-8");
38+
await globalThis.__next_route_preloader("warmerEvent");
3839
return;
3940
}
4041

‎packages/open-next/src/types/global.ts

+9
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,13 @@ declare global {
211211
* Defined in `createMainHandler`
212212
*/
213213
var cdnInvalidationHandler: CDNInvalidationHandler;
214+
215+
/**
216+
* A function to preload the routes.
217+
* This needs to be defined on globalThis because it can be used by custom overrides.
218+
* Only available in main functions.
219+
*/
220+
var __next_route_preloader: (
221+
stage: "waitUntil" | "start" | "warmerEvent" | "onDemand",
222+
) => Promise<void>;
214223
}

‎packages/open-next/src/types/open-next.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ export interface ResolvedRoute {
118118
type: RouteType;
119119
}
120120

121+
/**
122+
* The route preloading behavior. Only supported in Next 15+.
123+
* Default behavior of Next is disabled. You should do your own testing to choose which one suits you best
124+
* - "none" - No preloading of the route at all. This is the default
125+
* - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none". At the moment only cloudflare wrappers provide a `waitUntil`
126+
* - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now
127+
* - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation. The handler will only be created after all the routes have been loaded, it may increase the cold start time by a lot in some cases. Useful for long running server or in serverless with some careful testing
128+
* @default "none"
129+
*/
130+
export type RoutePreloadingBehavior =
131+
| "none"
132+
| "withWaitUntil"
133+
| "onWarmerEvent"
134+
| "onStart";
135+
121136
export interface RoutingResult {
122137
internalEvent: InternalEvent;
123138
// If the request is an external rewrite, if used with an external middleware will be false on every server function
@@ -170,7 +185,6 @@ export type IncludedWarmer = "aws-lambda" | "dummy";
170185
export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy";
171186

172187
export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy";
173-
174188
export interface DefaultOverrideOptions<
175189
E extends BaseEventOrResult = InternalEvent,
176190
R extends BaseEventOrResult = InternalResult,
@@ -313,6 +327,8 @@ export interface FunctionOptions extends DefaultFunctionOptions {
313327
* @deprecated This is not supported in 14.2+
314328
*/
315329
experimentalBundledNextServer?: boolean;
330+
331+
routePreloadingBehavior?: RoutePreloadingBehavior;
316332
}
317333

318334
export type RouteTemplate =

0 commit comments

Comments
 (0)