From 212d3b42edc9154ab6f94a4229f769315fb3c0e2 Mon Sep 17 00:00:00 2001 From: shramko Date: Fri, 10 Apr 2026 08:52:49 -0400 Subject: [PATCH] [Fizz] Fix duplicate preload links in SSR when Flight hints preload a rendered resource (#35889) When Flight encounters a `` JSX element during server rendering, it emits a preload hint while still serializing the element in the payload. During SSR, Fizz processes both the hint (via `preload()`) and the element (via `pushLink()`), resulting in duplicate `` tags in the HTML output. Add deduplication in `pushLink()` for `rel="preload"` links by checking and registering in `resumableState` before emitting, matching the same resource tracking used by the imperative `preload()` API. --- .../src/server/ReactFizzConfigDOM.js | 60 +++++++++++++++++ .../src/__tests__/ReactFlightDOM-test.js | 64 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 73ce8d3dd29f..f3c83c8085ff 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -3017,6 +3017,66 @@ function pushLink( // them up when the primary content is ready. They are never hydrated on the client anyway because // boundaries in fallback are awaited or client render, in either case there is never hydration return null; + } else if (rel === 'preload') { + // Preload links can be deduplicated against preloads initiated via the + // imperative preload() API or received as Flight hints. We check and + // register in resumableState to avoid duplicate link tags. + const as = props.as; + if (typeof as === 'string') { + switch (as) { + case 'image': { + const imageSrcSet = + typeof props.imageSrcSet === 'string' + ? props.imageSrcSet + : undefined; + const imageSizes = + typeof props.imageSizes === 'string' + ? props.imageSizes + : undefined; + const key = getImageResourceKey(href, imageSrcSet, imageSizes); + if (resumableState.imageResources.hasOwnProperty(key)) { + return null; + } + resumableState.imageResources[key] = PRELOAD_NO_CREDS; + break; + } + case 'style': { + const key = getResourceKey(href); + if (resumableState.styleResources.hasOwnProperty(key)) { + return null; + } + resumableState.styleResources[key] = PRELOAD_NO_CREDS; + break; + } + case 'script': { + const key = getResourceKey(href); + if (resumableState.scriptResources.hasOwnProperty(key)) { + return null; + } + resumableState.scriptResources[key] = PRELOAD_NO_CREDS; + break; + } + default: { + // font, audio, video, document, embed, fetch, object, track, worker, and others + const key = getResourceKey(href); + const hasAsType = + resumableState.unknownResources.hasOwnProperty(as); + let resources; + if (hasAsType) { + resources = resumableState.unknownResources[as]; + if (resources.hasOwnProperty(key)) { + return null; + } + } else { + resources = ({}: ResumableState['unknownResources']['asType']); + resumableState.unknownResources[as] = resources; + } + resources[key] = PRELOAD_NO_CREDS; + break; + } + } + } + return pushLinkImpl(renderState.hoistableChunks, props); } else { return pushLinkImpl(renderState.hoistableChunks, props); } diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 4abc18843050..929a54e74e5a 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -2166,6 +2166,70 @@ describe('ReactFlightDOM', () => { ); }); + it('does not emit duplicate preload links in SSR when Flight hints preload a resource that is also rendered as JSX', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + return ( +
+ + +
+ ); + } + + const {writable: flightWritable, readable: flightReadable} = + getTestStream(); + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + const {pipe} = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ), + ); + pipe(flightWritable); + + let response = null; + function getResponse() { + if (response === null) { + response = + ReactServerDOMClient.createFromReadableStream(flightReadable); + } + return response; + } + + function App() { + return ( + + {getResponse()} + + ); + } + + await serverAct(async () => { + ReactDOMFizzServer.renderToPipeableStream().pipe(fizzWritable); + }); + + await readInto(document, fizzReadable); + + // The font preload should appear only once, not twice + const preloadLinks = document.querySelectorAll( + 'link[rel="preload"][as="font"]', + ); + expect(preloadLinks.length).toBe(1); + }); + it('should be able to include a client reference in printed errors', async () => { const reportedErrors = [];