Skip to content

[Fizz] Deduplicate <link rel="preload"> from Flight preload hints#607

Open
everettbu wants to merge 1 commit into
mainfrom
fix/rsc-font-preload-duplicate-35889
Open

[Fizz] Deduplicate <link rel="preload"> from Flight preload hints#607
everettbu wants to merge 1 commit into
mainfrom
fix/rsc-font-preload-duplicate-35889

Conversation

@everettbu

Copy link
Copy Markdown

Mirror of facebook/react#35892
Original author: lllomh


Summary

Fixes #35889

<link rel="preload" as="font" /> (and other preload types) rendered
inside an RSC component was appearing twice in SSR HTML when used
with Fizz SSR. This regression was introduced in 19.2.

Root cause:

When the RSC Flight server processes a <link rel="preload"> element, it
does two things:

  1. Emits a Flight hint ('L' row) via processLink() in
    ReactFlightServerConfigDOM.js
  2. Includes the <link> element in the serialized component output

During Fizz SSR, the Flight hint is received and dispatched to the Fizz
preload() function, which registers the resource in resumableState
and adds it to renderState.fontPreloads. Later, the same <link>
element from the component output is processed by pushLink(), which
fell through to pushLinkImpl(renderState.hoistableChunks, props) with
no deduplication check — producing a second identical link tag.

Fix:

In pushLink(), before emitting a <link rel="preload"> to
hoistableChunks, check whether the resource is already registered in
resumableState (using the same per-as-type tracking that preload()
uses: imageResources, styleResources, scriptResources, and
unknownResources for fonts and others). If already registered, return
null. If not yet registered, register it so future preload() calls
will also deduplicate correctly.

Test plan

  • Added regression test does not duplicate font preload links from Flight hints during Fizz SSR in ReactFlightDOM-test.js that renders
    a <link rel="preload" as="font"> inside an RSC component through Fizz
    SSR and asserts the link appears exactly once in the HTML output.

@greptile-apps

greptile-apps Bot commented Feb 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a duplicate <link rel="preload"> bug in Fizz SSR when used with RSC Flight hints (regression in 19.2). It adds deduplication logic in pushLink() that checks resumableState before emitting preload link tags to hoistableChunks, mirroring the same per-as-type resource tracking used by the imperative preload() API.

  • Core fix in ReactFizzConfigDOM.js: Before emitting a <link rel="preload"> to hoistableChunks, checks whether the resource is already registered in resumableState (imageResources, styleResources, scriptResources, unknownResources). Returns null if already registered; otherwise registers it for future dedup.
  • Regression test added in ReactFlightDOM-test.js covering the as="font" case through the full RSC → Fizz pipeline.
  • Infrastructure changes: Yarn Berry migration (.yarnrc.yml, yarn.lock regeneration) and package.json formatting cleanup across 11 packages — unrelated to the bug fix and add significant diff noise.

Confidence Score: 4/5

  • The core fix is correct and well-targeted; the dedup logic mirrors existing patterns in the codebase.
  • The deduplication logic correctly follows the same resource tracking structure used by the imperative preload() function. The fix addresses a real regression and includes a regression test. One minor style concern: credential info (crossOrigin/integrity) is not preserved for style/script preloads, which differs from the preload() API behavior — though this is unlikely to cause issues in practice. The bulk of the diff (yarn.lock, package.json formatting) is infrastructure noise unrelated to the fix.
  • Pay close attention to packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js — the credential storage gap for style/script preloads.

Important Files Changed

Filename Overview
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js Core fix: adds deduplication logic in pushLink() for <link rel="preload"> elements against resources already registered in resumableState. Correctly mirrors the preload() function's resource tracking structure but always stores PRELOAD_NO_CREDS instead of preserving crossOrigin/integrity for style/script preloads.
packages/react-server-dom-webpack/src/tests/ReactFlightDOM-test.js Adds regression test that verifies font preload links from Flight hints are not duplicated during Fizz SSR. Test covers the core scenario well but only tests the as="font" case.
.yarnrc.yml New file adding Yarn Berry nodeLinker configuration. Infrastructure change unrelated to the bug fix.
yarn.lock Massive lockfile regeneration from Yarn 1 to Yarn Berry format. Infrastructure change unrelated to the bug fix.

Last reviewed commit: 3968a8d

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

if (resumableState.imageResources.hasOwnProperty(key)) {
return null;
}
resumableState.imageResources[key] = PRELOAD_NO_CREDS;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Credentials not preserved for style/script preloads

For as="style" and as="script", the preload() function (lines 6470-6475, 6493-6498) conditionally stores [options.crossOrigin, options.integrity] when those props are present, so that later <link rel="stylesheet"> or <script> elements can adopt those credentials via adoptPreloadCredentials(). Here, all resource types unconditionally store PRELOAD_NO_CREDS.

This means if a <link rel="preload" as="style" crossorigin="anonymous" integrity="sha256-..."> is rendered in RSC and registers here before the Flight hint's preload() call, the credential information is lost in resumableState. A later <link rel="stylesheet"> for the same href would not be able to adopt crossOrigin/integrity from the preload state (the check at line 2952 preloadState.length === 2 would fail).

In practice this is likely low-impact since the <link> element itself will still be in the HTML with the correct attributes, and stylesheets typically carry their own credentials. But for consistency with the preload() API, the style and script cases could store credential info from props.crossOrigin and props.integrity when present.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Line: 3041

Comment:
**Credentials not preserved for style/script preloads**

For `as="style"` and `as="script"`, the `preload()` function (lines 6470-6475, 6493-6498) conditionally stores `[options.crossOrigin, options.integrity]` when those props are present, so that later `<link rel="stylesheet">` or `<script>` elements can adopt those credentials via `adoptPreloadCredentials()`. Here, all resource types unconditionally store `PRELOAD_NO_CREDS`.

This means if a `<link rel="preload" as="style" crossorigin="anonymous" integrity="sha256-...">` is rendered in RSC and registers here before the Flight hint's `preload()` call, the credential information is lost in `resumableState`. A later `<link rel="stylesheet">` for the same `href` would not be able to adopt `crossOrigin`/`integrity` from the preload state (the check at line 2952 `preloadState.length === 2` would fail).

In practice this is likely low-impact since the `<link>` element itself will still be in the HTML with the correct attributes, and stylesheets typically carry their own credentials. But for consistency with the `preload()` API, the `style` and `script` cases could store credential info from `props.crossOrigin` and `props.integrity` when present.

How can I resolve this? If you propose a fix, please make it concise.

@everettbu everettbu changed the title Fix: deduplicate <link rel="preload"> elements against Flight hint preloads in Fizz SSR [Fizz] Deduplicate <link rel="preload"> from Flight preload hints Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants