Repository navigation
font-display: optional fonts can switch to the fallback after hydration in dev #17377
Description
Describe the bug
During development, a font with font-display: optional can render on first paint and then switch to the fallback font when the page hydrates, and stay on the fallback (a layout shift). The same page built for production keeps the font.
What happens:
- SvelteKit inlines the page's CSS into one
<style data-sveltekit>so that development doesn't flash unstyled content. - When the client starts, Vite's client injects each imported stylesheet again as a
<style data-vite-dev-id>. initializethen removesstyle[data-sveltekit](client.js).
Removing a stylesheet that declares @font-face makes Chromium re-create the page's font faces, and the new faces load their fonts again. Vite serves dev files with Cache-Control: no-cache, so these loads are conditional requests: in the reproduction the font is requested as 200, 304 or 200, 304, 304, where the production build requests it once. If the page renders while such a request is in flight, the browser gives up on an optional font for the rest of the page's life.
I'll open a PR that inlines the @font-face rules in a style of their own that stays in the page, and removes the rest as before.
Reproduction
https://github.com/xyrolle/sveltekit-dev-font-fallback
The app has one @font-face with font-display: optional in src/app.css, imported by the root layout, with the font preloaded. probe.js loads the page with Playwright and prints, for each load, the font the text ended up rendered with (from DevTools), the status of each request for the font, and any layout shifts.
pnpm install
FONT_DELAY_MS=10 pnpm dev --port 5173 # delays the font responses by 10 ms
node probe.js # Playwright's Chromium
CHANNEL=chrome node probe.js # installed ChromeLoads that ended in the fallback font (16 each):
| Playwright's Chromium 153 | Chrome 154 | |
|---|---|---|
vite dev, FONT_DELAY_MS=0 |
0/16 | 0/16 |
vite dev, FONT_DELAY_MS=10 |
9/16 | 8/16 |
vite build + vite preview, FONT_DELAY_MS=10 |
0/16 | 0/16 |
On localhost this small app wins the race. In a larger app it also happened on localhost, intermittently.
Logs
$ CHANNEL=chrome node probe.js http://localhost:4263/ # FONT_DELAY_MS=10 pnpm dev --port 4263
154.0.8037.98 http://localhost:4263/ latency=0ms
load 1: rendered with Geist; font responses 200, 304; layout shifts []
load 2: rendered with Helvetica; font responses 200, 304; layout shifts [0.00025]
load 3: rendered with Helvetica; font responses 200, 304; layout shifts [0.00025]
load 4: rendered with Helvetica; font responses 200, 304; layout shifts [0.00025]
load 5: rendered with Geist; font responses 200, 304; layout shifts []
...System Info
System:
OS: macOS 27.2
CPU: (18) arm64 Apple M5 Pro
Memory: 1.89 GB / 64.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.16.0 - /Users/xyrolle/.nvm/versions/node/v24.16.0/bin/node
Yarn: 1.22.22 - /Users/xyrolle/.nvm/versions/node/v24.16.0/bin/yarn
npm: 11.13.0 - /Users/xyrolle/.nvm/versions/node/v24.16.0/bin/npm
pnpm: 11.28.3 - /Users/xyrolle/.nvm/versions/node/v24.16.0/bin/pnpm
Browsers:
Chrome: 154.0.8037.98
Safari: 27.2
npmPackages:
@sveltejs/kit: 3.0.1 => 3.0.1
@sveltejs/vite-plugin-svelte: 7.3.1 => 7.3.1
svelte: 5.57.2 => 5.57.2
vite: 8.3.3 => 8.3.3Severity
annoyance
Additional Information
The same happens on main (9b56543). Serving dev fonts with a long Cache-Control, as unjs/fontaine#665 does for fontless, avoids the fallback, but when the first font load is slow the re-created face then swaps the font in after first paint, which production doesn't do either.
Activity
svelte-triage-bot commentedon Oct 8, 2026
I reproduced the issue, but could not produce a safe, verified fix.
What I tried
font-display: optionalfonts can switch to the fallback after hydration in dev #17377" through the GitHub connection.: The issue is open, has no comments, and explicitly links open PR fix: keep the @font-face rules inlined during development through hydration #17378 as its proposed fix. The reporter describes preserving inlined @font-face rules through hydration.font-display: optionalfonts can switch to the fallback after hydration in dev #17377 and addresses the same cause by splitting font-face rules into a retained style element, with hydration and HMR handling.Why I stopped
An existing open PR already addresses this exact issue: "#17378". Per the duplicate-work stopping rule, stopped before reproducing or implementing another patch. No tests were run, and no branch, commit, or push was created.