Skip to content

Improve Perf #800

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 9 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/warm-pans-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@opennextjs/aws": patch
---

Some perf improvements :
- Eliminate unnecessary runtime imports (i.e. dev react dependencies and next precompiled dev or turbopack dependencies)
- Refactor route preloading to be either on-demand or using waitUntil or at the start or during warmerEvent.
- Add a global function to preload routes when needed.
8 changes: 6 additions & 2 deletions packages/open-next/src/build/createServerBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import * as buildHelper from "./helper.js";
import { installDependencies } from "./installDeps.js";
import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js";
import {
patchEnvVars,
patchFetchCacheForISR,
patchFetchCacheSetMissingWaitUntil,
patchNextServer,
patchUnstableCacheForISR,
} from "./patch/patchFetchCacheISR.js";
import { patchFetchCacheSetMissingWaitUntil } from "./patch/patchFetchCacheWaitUntil.js";
} from "./patch/patches/index.js";

interface CodeCustomization {
// These patches are meant to apply on user and next generated code
Expand Down Expand Up @@ -187,6 +189,8 @@ async function generateBundle(
patchFetchCacheSetMissingWaitUntil,
patchFetchCacheForISR,
patchUnstableCacheForISR,
patchNextServer,
patchEnvVars,
...additionalCodePatches,
]);

Expand Down
7 changes: 7 additions & 0 deletions packages/open-next/src/build/patch/patches/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
export { patchNextServer } from "./patchNextServer.js";
export {
patchFetchCacheForISR,
patchUnstableCacheForISR,
} from "./patchFetchCacheISR.js";
export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
58 changes: 58 additions & 0 deletions packages/open-next/src/build/patch/patches/patchEnvVar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createPatchCode } from "../astCodePatcher.js";
import type { CodePatcher } from "../codePatcher";

/**
* This function create an ast-grep rule to replace specific env var inside an if.
* This is used to avoid loading unnecessary deps at runtime
* @param envVar The env var that we want to replace
* @param value The value that we want to replace it with
* @returns
*/
export const envVarRuleCreator = (envVar: string, value: string) => `
rule:
kind: member_expression
pattern: process.env.${envVar}
inside:
kind: parenthesized_expression
stopBy: end
inside:
kind: if_statement
fix:
'${value}'
`;

export const patchEnvVars: CodePatcher = {
name: "patch-env-vars",
patches: [
// This patch will set the `NEXT_RUNTIME` env var to "node" to avoid loading unnecessary edge deps at runtime
{
versions: ">=15.0.0",
field: {
pathFilter: /module\.compiled\.js$/,
contentFilter: /process\.env\.NEXT_RUNTIME/,
patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')),
},
},
// This patch will set `NODE_ENV` to production to avoid loading unnecessary dev deps at runtime
{
versions: ">=15.0.0",
field: {
pathFilter:
/(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/,
contentFilter: /process\.env\.NODE_ENV/,
patchCode: createPatchCode(
envVarRuleCreator("NODE_ENV", '"production"'),
),
},
},
// This patch will set `TURBOPACK` env to false to avoid loading turbopack related deps at runtime
{
versions: ">=15.0.0",
field: {
pathFilter: /module\.compiled\.js$/,
contentFilter: /process\.env\.TURBOPACK/,
patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", "false")),
},
},
],
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Lang } from "@ast-grep/napi";
import { getCrossPlatformPathRegex } from "utils/regex.js";
import { createPatchCode } from "./astCodePatcher.js";
import type { CodePatcher } from "./codePatcher";
import { createPatchCode } from "../astCodePatcher.js";
import type { CodePatcher } from "../codePatcher.js";

export const fetchRule = `
rule:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getCrossPlatformPathRegex } from "utils/regex.js";
import { createPatchCode } from "./astCodePatcher.js";
import type { CodePatcher } from "./codePatcher";
import { createPatchCode } from "../astCodePatcher.js";
import type { CodePatcher } from "../codePatcher.js";

export const rule = `
rule:
Expand Down
96 changes: 96 additions & 0 deletions packages/open-next/src/build/patch/patches/patchNextServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { createPatchCode } from "../astCodePatcher.js";
import type { CodePatcher } from "../codePatcher.js";

// 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)
export const minimalRule = `
rule:
kind: member_expression
pattern: process.env.NEXT_MINIMAL
any:
- inside:
kind: parenthesized_expression
stopBy: end
inside:
kind: if_statement
any:
- inside:
kind: statement_block
inside:
kind: method_definition
any:
- has: {kind: property_identifier, field: name, regex: runEdgeFunction}
- has: {kind: property_identifier, field: name, regex: runMiddleware}
- has: {kind: property_identifier, field: name, regex: imageOptimizer}
- has:
kind: statement_block
has:
kind: expression_statement
pattern: res.statusCode = 400;
fix:
'true'
`;

// This rule will disable the background preloading of route done by NextServer by default during the creation of NextServer
export const disablePreloadingRule = `
rule:
kind: statement_block
inside:
kind: if_statement
any:
- has:
kind: member_expression
pattern: this.nextConfig.experimental.preloadEntriesOnStart
stopBy: end
- has:
kind: binary_expression
pattern: appDocumentPreloading === true
stopBy: end
fix:
'{}'
`;

// This rule is mostly for splitted edge functions so that we don't try to match them on the other non edge functions
export const removeMiddlewareManifestRule = `
rule:
kind: statement_block
inside:
kind: method_definition
has:
kind: property_identifier
regex: getMiddlewareManifest
fix:
'{return null;}'
`;

export const patchNextServer: CodePatcher = {
name: "patch-next-server",
patches: [
// Skip executing next middleware, edge functions and image optimization inside NextServer
{
versions: ">=15.0.0",
field: {
pathFilter: /next-server\.(js)$/,
contentFilter: /process\.env\.NEXT_MINIMAL/,
patchCode: createPatchCode(minimalRule),
},
},
// Disable Next background preloading done at creation of `NetxServer`
{
versions: ">=15.0.0",
field: {
pathFilter: /next-server\.(js)$/,
contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/,
patchCode: createPatchCode(disablePreloadingRule),
},
},
// Don't match edge functions in `NextServer`
{
versions: ">=15.0.0",
field: {
pathFilter: /next-server\.(js)$/,
contentFilter: /getMiddlewareManifest/,
patchCode: createPatchCode(removeMiddlewareManifestRule),
},
},
],
};
3 changes: 3 additions & 0 deletions packages/open-next/src/core/createMainHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export async function createMainHandler() {
globalThis.serverId = generateUniqueId();
globalThis.openNextConfig = config;

// If route preloading behavior is set to start, it will wait for every single route to be preloaded before even creating the main handler.
await globalThis.__next_route_preloader("start");

// Default queue
globalThis.queue = await resolveQueue(thisFunction.override?.queue);

Expand Down
1 change: 1 addition & 0 deletions packages/open-next/src/core/requestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export async function openNextHandler(
waitUntil: options?.waitUntil,
},
async () => {
await globalThis.__next_route_preloader("waitUntil");
if (initialHeaders["x-forwarded-host"]) {
initialHeaders.host = initialHeaders["x-forwarded-host"];
}
Expand Down
49 changes: 48 additions & 1 deletion packages/open-next/src/core/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
// @ts-ignore
import NextServer from "next/dist/server/next-server.js";

import { debug } from "../adapters/logger.js";
import { debug, error } from "../adapters/logger.js";
import {
applyOverride as applyNextjsRequireHooksOverride,
overrideHooks as overrideNextjsRequireHooks,
Expand Down Expand Up @@ -59,6 +59,53 @@ const nextServer = new NextServer.default({
dir: __dirname,
});

let routesLoaded = false;

globalThis.__next_route_preloader = async (stage) => {
if (routesLoaded) {
return;
}
const thisFunction = globalThis.fnName
? globalThis.openNextConfig.functions![globalThis.fnName]
: globalThis.openNextConfig.default;
const routePreloadingBehavior =
thisFunction?.routePreloadingBehavior ?? "none";
if (routePreloadingBehavior === "none") {
routesLoaded = true;
return;
}
if (!("unstable_preloadEntries" in nextServer)) {
debug(
"The current version of Next.js does not support route preloading. Skipping route preloading.",
);
routesLoaded = true;
return;
}
if (stage === "waitUntil" && routePreloadingBehavior === "withWaitUntil") {
// We need to access the waitUntil
const waitUntil = globalThis.__openNextAls.getStore()?.waitUntil;
if (!waitUntil) {
error(
"You've tried to use the 'withWaitUntil' route preloading behavior, but the 'waitUntil' function is not available.",
);
routesLoaded = true;
return;
}
debug("Preloading entries with waitUntil");
waitUntil?.(nextServer.unstable_preloadEntries());
routesLoaded = true;
} else if (
(stage === "start" && routePreloadingBehavior === "onStart") ||
(stage === "warmerEvent" && routePreloadingBehavior === "onWarmerEvent") ||
stage === "onDemand"
) {
const startTimestamp = Date.now();
debug("Preloading entries");
await nextServer.unstable_preloadEntries();
debug("Preloading entries took", Date.now() - startTimestamp, "ms");
routesLoaded = true;
}
};
// `getRequestHandlerWithMetadata` is not available in older versions of Next.js
// It is required to for next 15.2 to pass metadata for page router data route
export const requestHandler = (metadata: Record<string, any>) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const handler: WrapperHandler = async (handler, converter) =>
if ("type" in event) {
const result = await formatWarmerResponse(event);
responseStream.end(Buffer.from(JSON.stringify(result)), "utf-8");
await globalThis.__next_route_preloader("warmerEvent");
return;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/open-next/src/types/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,13 @@ declare global {
* Defined in `createMainHandler`
*/
var cdnInvalidationHandler: CDNInvalidationHandler;

/**
* A function to preload the routes.
* This needs to be defined on globalThis because it can be used by custom overrides.
* Only available in main functions.
*/
var __next_route_preloader: (
stage: "waitUntil" | "start" | "warmerEvent" | "onDemand",
) => Promise<void>;
}
17 changes: 16 additions & 1 deletion packages/open-next/src/types/open-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ export interface ResolvedRoute {
type: RouteType;
}

/**
* The route preloading behavior. Only supported in Next 15+.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest:

  • Add the default behavior of Next is disabled
  • Add some guidelines to pick a value

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no general guidelines really, it depends so much on the deployed app that everyone should do their own testing. I'll add a comment about that

* - "none" - No preloading of the route at all
* - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none"
* - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now
* - "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.
* @default "none"
*/
export type RoutePreloadingBehavior =
| "none"
| "withWaitUntil"
| "onWarmerEvent"
| "onStart";

export interface RoutingResult {
internalEvent: InternalEvent;
// If the request is an external rewrite, if used with an external middleware will be false on every server function
Expand Down Expand Up @@ -170,7 +184,6 @@ export type IncludedWarmer = "aws-lambda" | "dummy";
export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy";

export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy";

export interface DefaultOverrideOptions<
E extends BaseEventOrResult = InternalEvent,
R extends BaseEventOrResult = InternalResult,
Expand Down Expand Up @@ -313,6 +326,8 @@ export interface FunctionOptions extends DefaultFunctionOptions {
* @deprecated This is not supported in 14.2+
*/
experimentalBundledNextServer?: boolean;

routePreloadingBehavior?: RoutePreloadingBehavior;
}

export type RouteTemplate =
Expand Down
Loading
Loading