Skip to content

Commit

Permalink
ensure preloads are inside of a render scope
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Jun 23, 2024
1 parent 641f529 commit 21c2294
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 18 deletions.
21 changes: 20 additions & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
RenderOpts,
Segment,
CacheNodeSeedData,
PreloadCallbacks,
} from './types'
import type { StaticGenerationStore } from '../../client/components/static-generation-async-storage.external'
import type { RequestStore } from '../../client/components/request-async-storage.external'
Expand Down Expand Up @@ -326,6 +327,8 @@ async function generateFlight(
flightRouterState,
} = ctx

const preloadCallbacks: PreloadCallbacks = []

if (!options?.skipFlight) {
const [MetadataTree, MetadataOutlet] = createMetadataComponents({
tree: loaderTree,
Expand Down Expand Up @@ -354,6 +357,7 @@ async function generateFlight(
rootLayoutIncluded: false,
asNotFound: ctx.isNotFoundPath || options?.asNotFound,
metadataOutlet: <MetadataOutlet />,
preloadCallbacks,
})
).map((path) => path.slice(1)) // remove the '' (root) segment
}
Expand Down Expand Up @@ -486,6 +490,8 @@ async function getRootAppProps(
createDynamicallyTrackedSearchParams,
})

const preloadCallbacks: PreloadCallbacks = []

const { seedData, styles } = await createComponentTree({
ctx,
createSegmentPath: (child) => child,
Expand All @@ -499,6 +505,7 @@ async function getRootAppProps(
asNotFound: asNotFound,
metadataOutlet: <MetadataOutlet />,
missingSlots,
preloadCallbacks,
})

// When the `vary` response header is present with `Next-URL`, that means there's a chance
Expand All @@ -519,6 +526,7 @@ async function getRootAppProps(
)

return {
P: <Preloads preloadCallbacks={preloadCallbacks} />,
b: ctx.renderOpts.buildId,
p: ctx.assetPrefix,
c: url.pathname + url.search,
Expand All @@ -530,7 +538,18 @@ async function getRootAppProps(
l: styles,
m: missingSlots,
G: GlobalError,
} as InitialRSCPayload
} as InitialRSCPayload & { P: React.ReactNode }
}

/**
* Preload calls (such as `ReactDOM.preloadStyle` and `ReactDOM.preloadFont`) need to be called during rendering
* in order to create the appropriate preload tags in the DOM, otherwise they're a no-op. Since we invoke
* renderToReadableStream with a function that returns component props rather than a component itself, we use
* this component to "render " the preload calls.
*/
function Preloads({ preloadCallbacks }: { preloadCallbacks: Function[] }) {
preloadCallbacks.forEach((preloadFn) => preloadFn())
return null
}

// This is the root component that runs in the RSC context
Expand Down
15 changes: 14 additions & 1 deletion packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { FlightSegmentPath, CacheNodeSeedData } from './types'
import type {
FlightSegmentPath,
CacheNodeSeedData,
PreloadCallbacks,
} from './types'
import React, { type ReactNode } from 'react'
import { isClientReference } from '../../lib/client-reference'
import { getLayoutOrPageModule } from '../lib/app-dir-module'
Expand All @@ -19,6 +23,7 @@ import type { LoadingModuleData } from '../../shared/lib/app-router-context.shar
type ComponentTree = {
seedData: CacheNodeSeedData
styles: ReactNode
preloadCallbacks: PreloadCallbacks
}

type Params = {
Expand All @@ -41,6 +46,7 @@ export function createComponentTree(props: {
metadataOutlet?: React.ReactNode
ctx: AppRenderContext
missingSlots?: Set<string>
preloadCallbacks: (() => void)[]
}): Promise<ComponentTree> {
return getTracer().trace(
NextNodeServerSpan.createComponentTree,
Expand All @@ -64,6 +70,7 @@ async function createComponentTreeInternal({
metadataOutlet,
ctx,
missingSlots,
preloadCallbacks,
}: {
createSegmentPath: CreateSegmentPath
loaderTree: LoaderTree
Expand All @@ -77,6 +84,7 @@ async function createComponentTreeInternal({
metadataOutlet?: React.ReactNode
ctx: AppRenderContext
missingSlots?: Set<string>
preloadCallbacks: PreloadCallbacks
}): Promise<ComponentTree> {
const {
renderOpts: { nextConfigOutput, experimental },
Expand Down Expand Up @@ -109,6 +117,7 @@ async function createComponentTreeInternal({
)

const layerAssets = getLayerAssets({
preloadCallbacks,
ctx,
layoutOrPagePath,
injectedCSS: injectedCSSWithCurrentLayout,
Expand Down Expand Up @@ -446,6 +455,7 @@ async function createComponentTreeInternal({
metadataOutlet: isChildrenRouteKey ? metadataOutlet : undefined,
ctx,
missingSlots,
preloadCallbacks,
})

currentStyles = childComponentStyles
Expand Down Expand Up @@ -509,6 +519,7 @@ async function createComponentTreeInternal({
loadingData,
],
styles: layerAssets,
preloadCallbacks,
}
}

Expand Down Expand Up @@ -539,6 +550,7 @@ async function createComponentTreeInternal({
loadingData,
],
styles: layerAssets,
preloadCallbacks,
}
}

Expand Down Expand Up @@ -631,5 +643,6 @@ async function createComponentTreeInternal({
loadingData,
],
styles: layerAssets,
preloadCallbacks,
}
}
39 changes: 25 additions & 14 deletions packages/next/src/server/app-render/get-layer-assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import { getPreloadableFonts } from './get-preloadable-fonts'
import type { AppRenderContext } from './app-render'
import { getAssetQueryString } from './get-asset-query-string'
import { encodeURIPath } from '../../shared/lib/encode-uri-path'
import type { PreloadCallbacks } from './types'

export function getLayerAssets({
ctx,
layoutOrPagePath,
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
preloadCallbacks,
}: {
layoutOrPagePath: string | undefined
injectedCSS: Set<string>
injectedJS: Set<string>
injectedFontPreloadTags: Set<string>
ctx: AppRenderContext
preloadCallbacks: PreloadCallbacks
}): React.ReactNode {
const { styles: styleTags, scripts: scriptTags } = layoutOrPagePath
? getLinkAndScriptTags(
Expand All @@ -43,21 +46,28 @@ export function getLayerAssets({
const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFilename)![1]
const type = `font/${ext}`
const href = `${ctx.assetPrefix}/_next/${encodeURIPath(fontFilename)}`
ctx.componentMod.preloadFont(
href,
type,
ctx.renderOpts.crossOrigin,
ctx.nonce
)

preloadCallbacks.push(() => {
ctx.componentMod.preloadFont(
href,
type,
ctx.renderOpts.crossOrigin,
ctx.nonce
)
})
}
} else {
try {
let url = new URL(ctx.assetPrefix)
ctx.componentMod.preconnect(url.origin, 'anonymous', ctx.nonce)
preloadCallbacks.push(() => {
ctx.componentMod.preconnect(url.origin, 'anonymous', ctx.nonce)
})
} catch (error) {
// assetPrefix must not be a fully qualified domain name. We assume
// we should preconnect to same origin instead
ctx.componentMod.preconnect('/', 'anonymous', ctx.nonce)
preloadCallbacks.push(() => {
ctx.componentMod.preconnect('/', 'anonymous', ctx.nonce)
})
}
}
}
Expand All @@ -83,12 +93,13 @@ export function getLayerAssets({
const precedence =
process.env.NODE_ENV === 'development' ? 'next_' + href : 'next'

ctx.componentMod.preloadStyle(
fullHref,
ctx.renderOpts.crossOrigin,
ctx.nonce
)

preloadCallbacks.push(() => {
ctx.componentMod.preloadStyle(
fullHref,
ctx.renderOpts.crossOrigin,
ctx.nonce
)
})
return (
<link
rel="stylesheet"
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 @@ -188,3 +188,5 @@ export interface RenderOptsPartial {
export type RenderOpts = LoadComponentsReturnType<AppPageModule> &
RenderOptsPartial &
RequestLifecycleOpts

export type PreloadCallbacks = (() => void)[]
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
FlightDataPath,
FlightRouterState,
FlightSegmentPath,
PreloadCallbacks,
Segment,
} from './types'
import type React from 'react'
Expand Down Expand Up @@ -42,6 +43,7 @@ export async function walkTreeWithFlightRouterState({
asNotFound,
metadataOutlet,
ctx,
preloadCallbacks,
}: {
createSegmentPath: CreateSegmentPath
loaderTreeToFilter: LoaderTree
Expand All @@ -57,6 +59,7 @@ export async function walkTreeWithFlightRouterState({
asNotFound?: boolean
metadataOutlet: React.ReactNode
ctx: AppRenderContext
preloadCallbacks: PreloadCallbacks
}): Promise<FlightDataPath[]> {
const {
renderOpts: { nextFontManifest, experimental },
Expand Down Expand Up @@ -154,6 +157,7 @@ export async function walkTreeWithFlightRouterState({
rootLayoutIncluded,
asNotFound,
metadataOutlet,
preloadCallbacks,
}
)

Expand All @@ -165,6 +169,7 @@ export async function walkTreeWithFlightRouterState({
injectedCSS: new Set(injectedCSS),
injectedJS: new Set(injectedJS),
injectedFontPreloadTags: new Set(injectedFontPreloadTags),
preloadCallbacks,
})

return [
Expand Down Expand Up @@ -226,6 +231,7 @@ export async function walkTreeWithFlightRouterState({
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
asNotFound,
metadataOutlet,
preloadCallbacks,
})

return path
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/app-dir/app-css/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,9 +482,9 @@ describe('app dir - css', () => {
counts.set(match, (counts.get(match) || 0) + 1)
}
for (const count of counts.values()) {
// There are 2 matches, one for the rendered <link> and one for float preload
// There are 3 matches, one for the rendered <link>, one for float preload and one for the <link> inside flight payload.
// And there is one match for the not found style
expect([1, 2]).toContain(count)
expect([1, 3]).toContain(count)
}
}
})
Expand Down

0 comments on commit 21c2294

Please sign in to comment.