Skip to content

Improve Perf #800

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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.
- 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
4 changes: 4 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,4 @@
export * from "./patchEnvVar.js";
export * from "./patchNextServer.js";
export * from "./patchFetchCacheISR.js";
export * from "./patchFetchCacheWaitUntil.js";
46 changes: 46 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,46 @@
import { createPatchCode } from "../astCodePatcher.js";
import type { CodePatcher } from "../codePatcher";

export const envVarRuleCreator = (envVar: string, value: string) => `
rule:
kind: member_expression
pattern: process.env.${envVar}
inside:
kind: if_statement
stopBy: end
fix:
'${value}'
`;

export const patchEnvVars: CodePatcher = {
name: "patch-env-vars",
patches: [
{
versions: ">=15.0.0",
field: {
pathFilter: /module\.compiled\.js$/,
contentFilter: /process\.env\.NEXT_RUNTIME/,
patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')),
},
},
{
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"'),
),
},
},
{
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
91 changes: 91 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,91 @@
import { createPatchCode } from "../astCodePatcher.js";
import type { CodePatcher } from "../codePatcher.js";

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'
`;

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: [
{
versions: ">=15.0.0",
field: {
pathFilter: /next-server\.(js)$/,
contentFilter: /process\.env\.NEXT_MINIMAL/,
patchCode: createPatchCode(minimalRule),
},
},
{
versions: ">=15.0.0",
field: {
pathFilter: /next-server\.(js)$/,
contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/,
patchCode: createPatchCode(disablePreloadingRule),
},
},
{
versions: ">=15.0.0",
field: {
pathFilter: /next-server\.(js)$/,
contentFilter: /getMiddlewareManifest/,
patchCode: createPatchCode(removeMiddlewareManifestRule),
},
},
],
};
2 changes: 2 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,8 @@ export async function createMainHandler() {
globalThis.serverId = generateUniqueId();
globalThis.openNextConfig = config;

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
47 changes: 46 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,51 @@ const nextServer = new NextServer.default({
dir: __dirname,
});

let alreadyLoaded = false;

globalThis.__next_route_preloader = async (stage) => {
if (alreadyLoaded) {
return;
}
const thisFunction = globalThis.fnName
? globalThis.openNextConfig.functions![globalThis.fnName]
: globalThis.openNextConfig.default;
const routePreloadingBehavior =
thisFunction?.routePreloadingBehavior ?? "none";
if (routePreloadingBehavior === "none") {
alreadyLoaded = true;
return;
}
if (!("unstable_preloadEntries" in nextServer)) {
debug(
"The current version of Next.js does not support route preloading. Skipping route preloading.",
);
alreadyLoaded = 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.",
);
}
debug("Preloading entries with waitUntil");
waitUntil?.(nextServer.unstable_preloadEntries());
alreadyLoaded = 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");
alreadyLoaded = 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,12 @@ export interface ResolvedRoute {
type: RouteType;
}

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 +176,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 +318,16 @@ export interface FunctionOptions extends DefaultFunctionOptions {
* @deprecated This is not supported in 14.2+
*/
experimentalBundledNextServer?: boolean;

/**
* The route preloading behavior. Only supported in Next 15+.
* - "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 and if not used properly, may increase the cold start time by a lot
* @default "none"
*/
routePreloadingBehavior?: RoutePreloadingBehavior;
}

export type RouteTemplate =
Expand Down
Loading
Loading