Skip to content

Commit ab6ec9e

Browse files
committed
fix(perf): exclude /_next/static/* from generated functions
1 parent 96ff784 commit ab6ec9e

File tree

5 files changed

+80
-4
lines changed

5 files changed

+80
-4
lines changed

src/build/functions/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const getHandlerFile = async (ctx: PluginContext): Promise<string> => {
106106

107107
const templateVariables: Record<string, string> = {
108108
'{{useRegionalBlobs}}': ctx.useRegionalBlobs.toString(),
109+
'{{excludeStaticPath}}': join(ctx.buildConfig.basePath || '', '/_next/static/*'),
109110
}
110111
// In this case it is a monorepo and we need to use a own template for it
111112
// as we have to change the process working directory

src/build/templates/handler-monorepo.tmpl.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,9 @@ export default async function (req, context) {
4949
export const config = {
5050
path: '/*',
5151
preferStatic: true,
52+
excludedPath: [
53+
// We use `preferStatic: true` so we already won't run this on *existing* static assets,
54+
// but by excluding this entire path we also avoid invoking the function just to 404.
55+
'{{excludeStaticPath}}',
56+
],
5257
}

src/build/templates/handler.tmpl.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,9 @@ export default async function handler(req, context) {
4343
export const config = {
4444
path: '/*',
4545
preferStatic: true,
46+
excludedPath: [
47+
// We use `preferStatic: true` so we already won't run this on *existing* static assets,
48+
// but by excluding this entire path we also avoid invoking the function just to 404.
49+
'{{excludeStaticPath}}',
50+
],
4651
}

tests/e2e/page-router.test.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { expect } from '@playwright/test'
22
import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'
33
import { test } from '../utils/playwright-helpers.js'
4+
import { join } from 'node:path'
5+
import { readdir } from 'node:fs/promises'
46

57
export function waitFor(millis: number) {
68
return new Promise((resolve) => setTimeout(resolve, millis))
@@ -614,6 +616,34 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
614616
/(s-maxage|max-age)/,
615617
)
616618
})
619+
620+
test.describe('static assets and function invocations', () => {
621+
test('should return 200 for an existing static asset without invoking a function', async ({
622+
page,
623+
pageRouter,
624+
}) => {
625+
// Since assets are hashed, we can't hardcode anything here. Find something to fetch.
626+
const [staticAsset] = await readdir(
627+
join(pageRouter.isolatedFixtureRoot, '.next', 'static', 'chunks'),
628+
)
629+
expect(staticAsset).toBeDefined()
630+
631+
const response = await page.goto(`${pageRouter.url}/_next/static/chunks/${staticAsset}`)
632+
633+
expect(response?.status()).toBe(200)
634+
expect(response?.headers()).not.toHaveProperty('x-nf-function-type')
635+
})
636+
637+
test('should return 404 for a nonexistent static asset without invoking a function', async ({
638+
page,
639+
pageRouter,
640+
}) => {
641+
const response = await page.goto(`${pageRouter.url}/_next/static/stale123abcdef.js`)
642+
643+
expect(response?.status()).toBe(404)
644+
expect(response?.headers()).not.toHaveProperty('x-nf-function-type')
645+
})
646+
})
617647
})
618648

619649
test.describe('Page Router with basePath and i18n', () => {
@@ -1352,9 +1382,9 @@ test.describe('Page Router with basePath and i18n', () => {
13521382

13531383
test('requesting a non existing page route that needs to be fetched from the blob store like 404.html', async ({
13541384
page,
1355-
pageRouter,
1385+
pageRouterBasePathI18n,
13561386
}) => {
1357-
const response = await page.goto(new URL('non-existing', pageRouter.url).href)
1387+
const response = await page.goto(new URL('non-existing', pageRouterBasePathI18n.url).href)
13581388
const headers = response?.headers() || {}
13591389
expect(response?.status()).toBe(404)
13601390

@@ -1375,9 +1405,9 @@ test.describe('Page Router with basePath and i18n', () => {
13751405

13761406
test('requesting a non existing page route that needs to be fetched from the blob store like 404.html (notFound: true)', async ({
13771407
page,
1378-
pageRouter,
1408+
pageRouterBasePathI18n,
13791409
}) => {
1380-
const response = await page.goto(new URL('static/not-found', pageRouter.url).href)
1410+
const response = await page.goto(new URL('static/not-found', pageRouterBasePathI18n.url).href)
13811411
const headers = response?.headers() || {}
13821412
expect(response?.status()).toBe(404)
13831413

@@ -1390,4 +1420,36 @@ test.describe('Page Router with basePath and i18n', () => {
13901420
)
13911421
expect(headers['cache-control']).toBe('public,max-age=0,must-revalidate')
13921422
})
1423+
1424+
test.describe('static assets and function invocations', () => {
1425+
test('should return 200 for an existing static asset without invoking a function', async ({
1426+
page,
1427+
pageRouterBasePathI18n,
1428+
}) => {
1429+
// Since assets are hashed, we can't hardcode anything here. Find something to fetch.
1430+
const [staticAsset] = await readdir(
1431+
join(pageRouterBasePathI18n.isolatedFixtureRoot, '.next', 'static', 'chunks'),
1432+
)
1433+
expect(staticAsset).toBeDefined()
1434+
1435+
const response = await page.goto(
1436+
`${pageRouterBasePathI18n.url}/_next/static/chunks/${staticAsset}`,
1437+
)
1438+
1439+
expect(response?.status()).toBe(200)
1440+
expect(response?.headers()).not.toHaveProperty('x-nf-function-type')
1441+
})
1442+
1443+
test('should return 404 for a nonexistent static asset without invoking a function', async ({
1444+
page,
1445+
pageRouterBasePathI18n,
1446+
}) => {
1447+
const response = await page.goto(
1448+
`${pageRouterBasePathI18n.url}/_next/static/stale123abcdef.js`,
1449+
)
1450+
1451+
expect(response?.status()).toBe(404)
1452+
expect(response?.headers()).not.toHaveProperty('x-nf-function-type')
1453+
})
1454+
})
13931455
})

tests/utils/create-e2e-fixture.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface DeployResult {
2121
deployID: string
2222
url: string
2323
logs: string
24+
isolatedFixtureRoot: string
2425
}
2526

2627
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'berry'
@@ -92,6 +93,7 @@ export const createE2EFixture = async (fixture: string, config: E2EConfig = {})
9293
cleanup: _cleanup,
9394
deployID: result.deployID,
9495
url: result.url,
96+
isolatedFixtureRoot: result.isolatedFixtureRoot,
9597
}
9698
} catch (error) {
9799
await _cleanup(true)
@@ -292,6 +294,7 @@ async function deploySite(
292294
url: `https://${deployID}--${siteName}.netlify.app`,
293295
deployID,
294296
logs: output,
297+
isolatedFixtureRoot,
295298
}
296299
}
297300

0 commit comments

Comments
 (0)