Repository navigation
Conversation
…ration During development the page's CSS is inlined to avoid FOUC, and removed once Vite's client has injected the same CSS again. Removing a stylesheet that declares `@font-face` makes the browser re-create the page's font faces, which then load their fonts again. Vite serves those with `Cache-Control: no-cache`, so the load is a network revalidation, and a `font-display: optional` font that is still loading when the page renders falls back for the rest of the page's life. Inline the top-level `@font-face` rules in a style of their own, and remove only the rest as before. The stylesheets are read the way browsers tokenize CSS. If any of them has a `@font-face` anywhere else, an `@import` or `@namespace`, or something a browser could read differently once it is split (a bad string or URL, a rule or comment left open at the end), they are all left whole. The client keeps that style only while each of its rules is also declared by one of the styles Vite injected into the head after it. It checks on hydration, after each HMR update and whenever the children of the head change (as when the styles of pruned modules are removed), so a face that the client never imports, or that was edited or removed, doesn't stay behind. The browser also re-creates the font faces when it takes in Vite's styles and the removal in one style update, so read a computed style before removing the inlined one.
|
Install the latest version of pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/e0e0966891147a66304f42dd674ac99516dc9976Open in Note This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed. |
🦋 Changeset detectedLatest commit: e0e0966 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
… each module once
|
I've replaced the implementation. The earlier version kept a second copy of the |
|
@xyrolle is attempting to deploy a commit to the vercelliott Team on Vercel. A member of the Team first needs to authorize it. |
closes #17377
During development, SvelteKit inlines the page's CSS in a
<style data-sveltekit>to avoid FOUC, and removes it on hydration, after Vite's client has injected the same CSS again in its own<style data-vite-dev-id>. A new stylesheet that declares@font-facecreates new font faces, so the page's fonts are requested again (a304). Afont-display: optionalfont that is still loading at that point falls back for the rest of the page's life (#17377).Vite's client already adopts styles rendered by the server: at startup it collects every
<style data-vite-dev-id>in the document, and when a CSS module runs,updateStylewrites to that element instead of adding a new one. So this PR:<style data-vite-dev-id="…">, marked with its module's id as the server resolved it, instead of one<style data-sveltekit>;page.jsimportingnested.js, which importsbase.css, and thenoverride.css, they came out asoverride.css,base.css. Onmainthat only lasts until hydration; with the elements kept, it would last.Production rendering is unchanged.
The font faces are kept only together with vitejs/vite#23697 ("fix(client): skip rewriting unchanged adopted styles", which fixes vitejs/vite#23696 and was released in Vite 8.3.4). Before it, Vite's client wrote the same CSS into the adopted
<style>again, and Chrome re-creates the font faces of a<style>whose text is assigned, even when the text is the same. Neither change keeps the font faces on its own. Even with both, Vite only skips the write when the text is identical: withcss.devSourcemap: true, the client's CSS has a source map comment that the inlined text doesn't, so the faces are re-created as onmain.Measured
A page that imports a stylesheet with an
@font-face(font-display: optional), undervite dev. The request for the stylesheet's module is held until the server-rendered font faces have been recorded, thendocument.fontsis compared after hydration. 3 loads each, after a warm-up load, in Chrome 155 and in Playwright's Chromium 153, with this PR's head:main304)304)The reproduction from #17377, a full project with a Playwright probe: https://github.com/xyrolle/sveltekit-dev-font-fallback
Open questions
Styles the client never takes over. Without the hydration cleanup, an inlined style whose module never runs in the browser under the same id stays until the page reloads. In the
basicsapp:if (import.meta.env.SSR) await import('./server-only.css')in a+page.js) stays after hydration and after client-side navigation, where its rules still apply to the next page;exportsresolve tonode.csson the server andbrowser.cssin the browser leaves both in the document after hydration.HTML can't carry the
\0that starts a virtual module's id, so an inlined virtual stylesheet would never be taken over either. The one in my probe (a plugin's\0virtual:probe.css) wasn't inlined at all, as onmain.Vite's client doesn't say which adopted styles a module claimed, so SvelteKit can't tell these apart from the ones it took over. Would you rather have a small signal from Vite's client for that, so that SvelteKit removes the unclaimed styles on hydration as it does today, or keep a cleanup in SvelteKit some other way?
Stylesheets only the client imports. Vite's client adds a stylesheet it hasn't adopted after the others, and doesn't move the adopted ones. A stylesheet that
hooks.client.jsimports (.probe { color: red }) therefore ends up after a page's stylesheet that overrides it (.probe { color: blue }), which the server inlined: in thebasicsapp the text is red after hydration, and blue onmain. Keeping the cascade here needs Vite to insert such a style before the adopted ones that come later in module order, or SvelteKit to know these stylesheets on the server.I'll keep this a draft until both are settled.
Tests
basics,client.test.js›CSS› "lets Vite take over the styles inlined during development" (development only): it holds back the request for/css's stylesheet module until it has found the inlined<style>, then checks that this element is still in the document after hydration and that the stylesheet isn't there twice. Onmainit fails, because the inlined style is removed and replaced. It doesn't check font faces, since SvelteKit's lockfile still has Vite 8.3.0.basics,cross-platform/test.js›CSS› "applies styles in the order they are imported": the nested import above, a+page.jsthat imports a module importing it back before its own stylesheet, and a stylesheet thatloadimports dynamically (checked in development and with JavaScript, since a built page doesn't link it from the server-rendered HTML). Onmainit fails in development without JavaScript; without the ordering changes it also fails with JavaScript.style[data-sveltekit].options›inlineStyleThreshold› "inlines CSS" now expects the inlined styles to stay in development, with and without JavaScript, and thebasicscheck for a page withssr = falselooks forstyle[data-vite-dev-id].On e0e0966, with
main(d955d1b) merged in,pnpm lint,pnpm checkandpnpm test:kit:unitpass (75 files, 1115 passed, 1 skipped), and so do all the test apps in development (pnpm test:kit:dev,basics: 717 passed), theoptionsapp's build tests and the ordering test against a build. I didn't run the other build suites.This replaces the earlier version of this PR, which kept a second copy of the
@font-facerules through hydration, with a CSS tokenizer and aMutationObserverto keep it in step with Vite's.Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
Tests
pnpm testand lint the project withpnpm lintandpnpm checkChangesets
pnpm changesetand following the prompts. Changesets that add features should beminorand those that fix bugs should bepatch. Please prefix changeset messages withfeat:,fix:, orchore:.Edits