From a26a6819c7a6348b6c7748be18bdb8589c6eaac3 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Wed, 29 Jan 2025 15:33:40 -0500 Subject: [PATCH] fix(perf): exclude /_next/static/ dir from function See inline comment. This only matters because of browsers requesting assets from previous deploys or bots crawling for garbage. This will avoid unnecessary function invocations in those cases, improving performance and reducing customer bills in some cases. --- src/build/templates/handler-monorepo.tmpl.js | 5 ++++ src/build/templates/handler.tmpl.js | 5 ++++ tests/e2e/simple-app.test.ts | 26 ++++++++++++++++++++ tests/utils/create-e2e-fixture.ts | 2 ++ 4 files changed, 38 insertions(+) diff --git a/src/build/templates/handler-monorepo.tmpl.js b/src/build/templates/handler-monorepo.tmpl.js index 89f4411d5e..4aa376f79a 100644 --- a/src/build/templates/handler-monorepo.tmpl.js +++ b/src/build/templates/handler-monorepo.tmpl.js @@ -53,4 +53,9 @@ export default async function (req, context) { export const config = { path: '/*', preferStatic: true, + excludedPath: [ + // We use `preferStatic: true` so we already won't run this on *existing* static assets, + // but by excluding this entire path we also avoid invoking the function just to 404. + '/_next/static/*', + ], } diff --git a/src/build/templates/handler.tmpl.js b/src/build/templates/handler.tmpl.js index c86fe13131..ac7207b72c 100644 --- a/src/build/templates/handler.tmpl.js +++ b/src/build/templates/handler.tmpl.js @@ -46,4 +46,9 @@ export default async function handler(req, context) { export const config = { path: '/*', preferStatic: true, + excludedPath: [ + // We use `preferStatic: true` so we already won't run this on *existing* static assets, + // but by excluding this entire path we also avoid invoking the function just to 404. + '/_next/static/*', + ], } diff --git a/tests/e2e/simple-app.test.ts b/tests/e2e/simple-app.test.ts index 980ad84dd0..81cf59c0fd 100644 --- a/tests/e2e/simple-app.test.ts +++ b/tests/e2e/simple-app.test.ts @@ -1,6 +1,8 @@ import { expect, type Locator } from '@playwright/test' import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs' import { test } from '../utils/playwright-helpers.js' +import { join } from 'node:path' +import { readdir } from 'node:fs/promises' const expectImageWasLoaded = async (locator: Locator) => { expect(await locator.evaluate((img: HTMLImageElement) => img.naturalHeight)).toBeGreaterThan(0) @@ -282,3 +284,27 @@ test('can require CJS module that is not bundled', async ({ simple }) => { expect(parsedBody.notBundledCJSModule.isBundled).toEqual(false) expect(parsedBody.bundledCJSModule.isBundled).toEqual(true) }) + +test('serves a 200 for an existing static asset without invoking a function', async ({ + page, + simple, +}) => { + // Since assets are hashed, we can't hardcode anything here. Find something to fetch. + const [staticAsset] = await readdir(join(simple.isolatedFixtureRoot, '.next', 'static', 'chunks')) + expect(staticAsset).toBeDefined() + + const response = await page.goto(`${simple.url}/_next/static/chunks/${staticAsset}`) + + expect(response?.status()).toBe(200) + expect(response?.headers()).not.toHaveProperty('x-nf-function-type') +}) + +test('serves a 404 for a nonexistent static asset without invoking a function', async ({ + page, + simple, +}) => { + const response = await page.goto(`${simple.url}/_next/static/stale123abcdef.js`) + + expect(response?.status()).toBe(404) + expect(response?.headers()).not.toHaveProperty('x-nf-function-type') +}) diff --git a/tests/utils/create-e2e-fixture.ts b/tests/utils/create-e2e-fixture.ts index cc9c588496..08367ff8f0 100644 --- a/tests/utils/create-e2e-fixture.ts +++ b/tests/utils/create-e2e-fixture.ts @@ -21,6 +21,7 @@ export interface DeployResult { deployID: string url: string logs: string + isolatedFixtureRoot: string } type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'berry' @@ -92,6 +93,7 @@ export const createE2EFixture = async (fixture: string, config: E2EConfig = {}) cleanup: _cleanup, deployID: result.deployID, url: result.url, + isolatedFixtureRoot, } } catch (error) { await _cleanup(true)