Skip to content

Commit

Permalink
prerendered pages should use static staleTime
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Jul 18, 2024
1 parent 669bd05 commit db435fa
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 60 deletions.
1 change: 1 addition & 0 deletions packages/next/src/client/components/app-router-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export const FLIGHT_HEADERS = [
export const NEXT_RSC_UNION_QUERY = '_rsc' as const

export const NEXT_DID_POSTPONE_HEADER = 'x-nextjs-postponed' as const
export const NEXT_IS_PRERENDER_HEADER = 'x-nextjs-prerender' as const
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ export function createInitialRouterState({
createPrefetchCacheEntryForInitialLoad({
url,
kind: PrefetchKind.AUTO,
data: { f: initialFlightData, c: undefined, i: !!couldBeIntercepted },
data: {
f: initialFlightData,
c: undefined,
i: !!couldBeIntercepted,
// TODO: the server should probably send a value for this. Default to false for now.
p: false,
},
tree: initialState.tree,
prefetchCache: initialState.prefetchCache,
nextUrl: initialState.nextUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
RSC_HEADER,
RSC_CONTENT_TYPE_HEADER,
NEXT_HMR_REFRESH_HEADER,
NEXT_IS_PRERENDER_HEADER,
} from '../app-router-headers'
import { callServer } from '../../app-call-server'
import { PrefetchKind } from './router-reducer-types'
Expand Down Expand Up @@ -59,6 +60,7 @@ function doMpaNavigation(url: string): FetchServerResponseResult {
f: urlToUrlWithoutFlightMarker(url).toString(),
c: undefined,
i: false,
p: false,
}
}

Expand Down Expand Up @@ -156,6 +158,7 @@ export async function fetchServerResponse(

const contentType = res.headers.get('content-type') || ''
const interception = !!res.headers.get('vary')?.includes(NEXT_URL)
const isPrerender = !!res.headers.get(NEXT_IS_PRERENDER_HEADER)
let isFlightResponse = contentType === RSC_CONTENT_TYPE_HEADER

if (process.env.NODE_ENV === 'production') {
Expand Down Expand Up @@ -193,6 +196,7 @@ export async function fetchServerResponse(
f: response.f,
c: canonicalUrl,
i: interception,
p: isPrerender,
}
} catch (err) {
console.error(
Expand All @@ -206,6 +210,7 @@ export async function fetchServerResponse(
f: url.toString(),
c: undefined,
i: false,
p: false,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ function prefixExistingPrefetchCacheEntry({
const newCacheKey = createPrefetchCacheKey(url, nextUrl)
prefetchCache.set(newCacheKey, existingCacheEntry)
prefetchCache.delete(existingCacheKey)

return newCacheKey
}

/**
Expand Down Expand Up @@ -201,8 +203,28 @@ function createLazyPrefetchEntry({
// TODO: `fetchServerResponse` should be more tighly coupled to these prefetch cache operations
// to avoid drift between this cache key prefixing logic
// (which is currently directly influenced by the server response)
let newCacheKey

if (prefetchResponse.i) {
prefixExistingPrefetchCacheEntry({ url, nextUrl, prefetchCache })
// Determine if we need to prefix the cache key with the nextUrl
newCacheKey = prefixExistingPrefetchCacheEntry({
url,
nextUrl,
prefetchCache,
})
}

// If the prefetch was a cache hit, we want to update the existing cache entry to reflect that it was a full prefetch.
// This is because we know that a static response will contain the full RSC payload, and can be updated to respect the `static`
// staleTime.
if (prefetchResponse.p) {
const existingCacheEntry = prefetchCache.get(
// if we prefixed the cache key due to route interception, we want to use the new key. Otherwise we use the original key
newCacheKey ?? prefetchCacheKey
)
if (existingCacheEntry) {
existingCacheEntry.kind = PrefetchKind.FULL
}
}

return prefetchResponse
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/export/routes/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { hasNextSupport } from '../../telemetry/ci-info'
import { lazyRenderAppPage } from '../../server/route-modules/app-page/module.render'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { NodeNextRequest, NodeNextResponse } from '../../server/base-http/node'
import { NEXT_IS_PRERENDER_HEADER } from '../../client/components/app-router-headers'

export const enum ExportedAppPageFiles {
HTML = 'HTML',
Expand Down Expand Up @@ -114,6 +115,9 @@ export async function exportAppPage(

const headers: OutgoingHttpHeaders = { ...metadata.headers }

// If we're writing the file to disk, we know it's a prerender.
headers[NEXT_IS_PRERENDER_HEADER] = '1'

if (fetchTags) {
headers[NEXT_CACHE_TAGS_HEADER] = fetchTags
}
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ export type FetchServerResponseResult = {
c: URL | undefined
/** couldBeIntercepted */
i: boolean
/** isPrerender */
p: boolean
}

export type RSCPayload =
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import {
NEXT_DID_POSTPONE_HEADER,
NEXT_URL,
NEXT_ROUTER_STATE_TREE_HEADER,
NEXT_IS_PRERENDER_HEADER,
} from '../client/components/app-router-headers'
import type {
MatchOptions,
Expand Down Expand Up @@ -2914,6 +2915,9 @@ export default abstract class Server<
? 'STALE'
: 'HIT'
)
// Set a header used by the client router to signal the response is static
// and should respect the `static` cache staleTime value.
res.setHeader(NEXT_IS_PRERENDER_HEADER, '1')
}

const { value: cachedData } = cacheEntry
Expand Down
133 changes: 75 additions & 58 deletions test/e2e/app-dir/app-prefetch/prefetching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,10 @@ const browserConfigWithFixedTime = {
}

describe('app dir - prefetching', () => {
const { next, isNextDev, skipped } = nextTestSetup({
const { next, isNextDev, isNextDeploy } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})

if (skipped) {
return
}

// TODO: re-enable for dev after https://vercel.slack.com/archives/C035J346QQL/p1663822388387959 is resolved (Sep 22nd 2022)
if (isNextDev) {
it('should skip next dev for now', () => {})
Expand Down Expand Up @@ -112,6 +107,23 @@ describe('app dir - prefetching', () => {
expect(
requests.filter((request) => request === '/static-page').length
).toBe(1)

// return to the home page
await browser.elementByCss('#to-home').click()
await browser.waitForElementByCss('#to-static-page')
// there shouldn't be any additional prefetches
expect(
requests.filter((request) => request === '/static-page').length
).toBe(1)

// navigate to the static page again
await browser.elementByCss('#to-static-page').click()
await browser.waitForElementByCss('#static-page')

// there still should only be the initial request to the static page
expect(
requests.filter((request) => request === '/static-page').length
).toBe(1)
})

it('should calculate `_rsc` query based on `Next-Url`', async () => {
Expand Down Expand Up @@ -289,60 +301,65 @@ describe('app dir - prefetching', () => {
await browser.waitForElementByCss('#prefetch-auto-page-data')
})

describe('dynamic rendering', () => {
describe.each(['/force-dynamic', '/revalidate-0'])('%s', (basePath) => {
it('should not re-render layout when navigating between sub-pages', async () => {
const logStartIndex = next.cliOutput.length

const browser = await next.browser(`${basePath}/test-page`)
let initialRandomNumber = await browser
.elementById('random-number')
.text()
await browser
.elementByCss(`[href="${basePath}/test-page/sub-page"]`)
.click()

await check(() => browser.hasElementByCssSelector('#sub-page'), true)

const newRandomNumber = await browser
.elementById('random-number')
.text()

expect(initialRandomNumber).toBe(newRandomNumber)

await check(() => {
const logOccurrences =
next.cliOutput.slice(logStartIndex).split('re-fetching in layout')
.length - 1

return logOccurrences
}, 1)
})

it('should update search params following a link click', async () => {
const browser = await next.browser(`${basePath}/search-params`)
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
await browser.elementByCss(`[href="${basePath}/search-params"]`).click()
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
// These tests are skipped when deployed as they rely on runtime logs
if (!isNextDeploy) {
describe('dynamic rendering', () => {
describe.each(['/force-dynamic', '/revalidate-0'])('%s', (basePath) => {
it('should not re-render layout when navigating between sub-pages', async () => {
const logStartIndex = next.cliOutput.length

const browser = await next.browser(`${basePath}/test-page`)
let initialRandomNumber = await browser
.elementById('random-number')
.text()
await browser
.elementByCss(`[href="${basePath}/test-page/sub-page"]`)
.click()

await check(() => browser.hasElementByCssSelector('#sub-page'), true)

const newRandomNumber = await browser
.elementById('random-number')
.text()

expect(initialRandomNumber).toBe(newRandomNumber)

await check(() => {
const logOccurrences =
next.cliOutput.slice(logStartIndex).split('re-fetching in layout')
.length - 1

return logOccurrences
}, 1)
})

it('should update search params following a link click', async () => {
const browser = await next.browser(`${basePath}/search-params`)
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
await browser
.elementByCss(`[href="${basePath}/search-params"]`)
.click()
await check(
() => browser.elementById('search-params-data').text(),
/{}/
)
await browser.elementByCss('[href="?foo=true"]').click()
await check(
() => browser.elementById('search-params-data').text(),
/{"foo":"true"}/
)
})
})
})
})
}

describe('invalid URLs', () => {
it('should not throw when an invalid URL is passed to Link', async () => {
Expand Down

0 comments on commit db435fa

Please sign in to comment.