Skip to content

Commit

Permalink
implement lazy loading via Pages' _worker.js directory (#191)
Browse files Browse the repository at this point in the history
* implement lazy loading via Pages' _worker.js directory

* improve getFunctionNestingLevel by using path.relative

* add (invalid) todo comment in package.json

* add wrangler peer dependency

* fix broken types

* add changeset
  • Loading branch information
dario-piotrowicz authored May 9, 2023
1 parent 86df485 commit 4d8a708
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 27 deletions.
6 changes: 6 additions & 0 deletions .changeset/slow-suits-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@cloudflare/next-on-pages': minor
---

utilize Wrangler new capability of dynamically importing code to avoid the evaluation/run of javascript code
when not necessary, reducing the app's startup time (which causes apps to often hit the script startup CPU time limit)
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"zodcli": "^0.0.4"
},
"peerDependencies": {
"vercel": "^28.0.2"
"vercel": "^28.0.2",
"wrangler": "^2.20.0"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
Expand Down
33 changes: 23 additions & 10 deletions src/buildApplication/buildWorkerFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { writeFile } from 'fs/promises';
import { join } from 'path';
import type { Plugin } from 'esbuild';
import { join } from 'path';
import { build } from 'esbuild';
import { tmpdir } from 'os';
import { cliSuccess } from '../cli';
Expand All @@ -14,17 +14,24 @@ import type { ProcessedVercelOutput } from './processVercelOutput';
* @returns Record for the build output map.
*/
export function constructBuildOutputRecord(item: BuildOutputItem) {
return item.type === 'static'
? `{ type: ${JSON.stringify(item.type)} }`
: item.type === 'override'
? `{
if (item.type === 'static') {
return `{ type: ${JSON.stringify(item.type)} }`;
}

if (item.type === 'override') {
return `{
type: ${JSON.stringify(item.type)},
path: ${item.path ? JSON.stringify(item.path) : undefined},
headers: ${item.headers ? JSON.stringify(item.headers) : undefined}
}`
: `{
}`;
}

return `{
type: ${JSON.stringify(item.type)},
entrypoint: AsyncLocalStoragePromise.then(() => import('${item.entrypoint}'))
entrypoint: AsyncLocalStoragePromise.then(() => import('${item.entrypoint.replace(
/^\.vercel\/output\/static\/_worker\.js\/__next-on-pages-dist__\//,
'./__next-on-pages-dist__/'
)}')),
}`;
}

Expand All @@ -49,7 +56,13 @@ export async function buildWorkerFile(
.join(',')}};`
);

const outputFile = join('.vercel', 'output', 'static', '_worker.js');
const outputFile = join(
'.vercel',
'output',
'static',
'_worker.js',
'index.js'
);

await build({
entryPoints: [join(__dirname, '..', 'templates', '_worker.js')],
Expand All @@ -60,7 +73,7 @@ export async function buildWorkerFile(
inject: [functionsFile],
target: 'es2022',
platform: 'neutral',
external: ['node:async_hooks', 'node:buffer'],
external: ['node:async_hooks', 'node:buffer', './__next-on-pages-dist__/*'],
define: {
__CONFIG__: JSON.stringify(vercelConfig),
},
Expand Down
75 changes: 59 additions & 16 deletions src/buildApplication/generateFunctionsMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from '../utils';
import type { CliOptions } from '../cli';
import { cliError, cliWarn } from '../cli';
import { tmpdir } from 'os';
import type * as AST from 'ast-types/gen/kinds';
import assert from 'node:assert';
import type { PrerenderedFileData } from './fixPrerenderedRoutes';
Expand All @@ -35,10 +34,18 @@ export async function generateFunctionsMap(
functionsDir: string,
disableChunksDedup: CliOptions['disableChunksDedup']
): Promise<DirectoryProcessingResults> {
const nextOnPagesDistDir = join(
'.vercel',
'output',
'static',
'_worker.js',
'__next-on-pages-dist__'
);

const processingSetup = {
functionsDir,
tmpFunctionsDir: join(tmpdir(), Math.random().toString(36).slice(2)),
tmpWebpackDir: join(tmpdir(), Math.random().toString(36).slice(2)),
distFunctionsDir: join(nextOnPagesDistDir, 'functions'),
distWebpackDir: join(nextOnPagesDistDir, 'chunks'),
disableChunksDedup,
};

Expand All @@ -52,7 +59,7 @@ export async function generateFunctionsMap(
if (!disableChunksDedup) {
await buildWebpackChunkFiles(
processingResults.webpackChunks,
processingSetup.tmpWebpackDir
processingSetup.distWebpackDir
);
}

Expand Down Expand Up @@ -217,14 +224,14 @@ async function processFuncDirectory(

if (!setup.disableChunksDedup) {
const { updatedFunctionContents, extractedWebpackChunks } =
extractWebpackChunks(setup.tmpWebpackDir, contents, webpackChunks);
extractWebpackChunks(contents, functionFile, webpackChunks);
contents = updatedFunctionContents;
extractedWebpackChunks.forEach((value, key) =>
webpackChunks.set(key, value)
);
}

const newFilePath = join(setup.tmpFunctionsDir, `${relativePath}.js`);
const newFilePath = join(setup.distFunctionsDir, `${relativePath}.js`);
await mkdir(dirname(newFilePath), { recursive: true });
await writeFile(newFilePath, contents);

Expand Down Expand Up @@ -282,16 +289,20 @@ function fixFunctionContents(contents: string) {
* those chunks
*/
function extractWebpackChunks(
tmpWebpackDir: string,
functionContents: string,
filePath: string,
existingWebpackChunks: Map<number, string>
): {
updatedFunctionContents: string;
extractedWebpackChunks: Map<number, string>;
} {
const getChunkImport = getChunkImportFn(filePath);

const webpackChunks = new Map<number, string>();
const webpackChunksCodeReplaceMap = new Map<string, string>();

const webpackChunksImports: string[] = [];

const parsedContents = parse(functionContents, {
ecmaVersion: 'latest',
sourceType: 'module',
Expand Down Expand Up @@ -323,15 +334,11 @@ function extractWebpackChunks(

webpackChunks.set(key, chunkExpressionCode);

const chunkFilePath = join(tmpWebpackDir, `${key}.js`);

const newChunkExpressionCode = `require(${JSON.stringify(
chunkFilePath
)}).default`;
webpackChunksImports.push(getChunkImport(key));

webpackChunksCodeReplaceMap.set(
chunkExpressionCode,
newChunkExpressionCode
getChunkIdentifier(key)
);
});

Expand All @@ -340,7 +347,9 @@ function extractWebpackChunks(
});

return {
updatedFunctionContents: functionContents,
updatedFunctionContents: [...webpackChunksImports, functionContents].join(
';\n'
),
extractedWebpackChunks: webpackChunks,
};
}
Expand Down Expand Up @@ -398,8 +407,8 @@ async function tryToFixFaviconFunc(): Promise<void> {

type ProcessingSetup = {
functionsDir: string;
tmpFunctionsDir: string;
tmpWebpackDir: string;
distFunctionsDir: string;
distWebpackDir: string;
disableChunksDedup: boolean;
};

Expand Down Expand Up @@ -478,3 +487,37 @@ function assertSelfWebpackChunk_N_E(expression: AST.NodeKind): void {
assert(expression.property.type === 'Identifier');
assert(expression.property.name === 'webpackChunk_N_E');
}

function getChunkIdentifier(chunkKey: number): string {
return `__chunk_${chunkKey}`;
}

function getChunkImportFn(functionPath: string): (chunkKey: number) => string {
const functionNestingLevel = getFunctionNestingLevel(functionPath);
const accountForNestingPath = `../`.repeat(functionNestingLevel);
return chunkKey => {
const chunkIdentifier = getChunkIdentifier(chunkKey);
const chunkPath = `${accountForNestingPath}__next-on-pages-dist__/chunks/${chunkKey}.js`;
return `import ${chunkIdentifier} from '${chunkPath}'`;
};
}

const functionsDir = resolve('.vercel', 'output', 'functions');

function getFunctionNestingLevel(functionPath: string): number {
let nestingLevel = -1;
try {
const relativePath = relative(functionPath, functionsDir);
nestingLevel = relativePath.split('..').length - 1;
} catch {
/* empty */
}

if (nestingLevel < 0) {
throw new Error(
`Error: could not determine nesting level of the following function: ${functionPath}`
);
}

return nestingLevel;
}

0 comments on commit 4d8a708

Please sign in to comment.