Skip to content

feat(design): preload a portal design's fonts during SSR - #856

Closed
mortondev wants to merge 2 commits into
feat/portal-design-v2-phase5-switchfrom
feat/portal-design-v2-phase5-font-preload
Closed

mortondev wants to merge 2 commits into
feat/portal-design-v2-phase5-switchfrom
feat/portal-design-v2-phase5-font-preload

Conversation

@mortondev

@mortondev mortondev commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

The twenty-third of the stack that builds a new design engine for the portal, and the fourth of phase 5. A portal page now links its design's fonts in the head during SSR, so text draws in its face from the first paint instead of swapping in after the page's script asks for it. Behind the portalDesignV2 flag.

How

  • A manifest from the client build. lib/build/font-manifest.ts is a Vite plugin. A family loads with a dynamic import() of its CSS module, so the client build gives its stylesheet and files hashed names. The plugin records, by font id, each module's stylesheet and its Latin woff2 files by weight, and hands them to the server build as virtual:quackback-font-manifest. The client is built first, as for the router's own manifest. It reads which module serves which id from font-loader.ts itself, so there is no second list to keep in step. In the dev server and tests the manifest is empty and fonts load as before.
  • The links, planned on the server. fontLinks() (domains/design/font-plan.ts) gives each bundled family's stylesheet and preloads at most two Latin files: the body at 400 and the headings at their weight, or the nearest weight the family has. Inter ships with the app's own stylesheet and needs neither. portalDesignView() adds them to the design view as fontLinks, so the browser's code only reads them, and the portal's head() appends them to its links.
  • The client's font loader is unchanged. Its dynamic import finds the stylesheet already linked and adds nothing.

Verification

  • Built, the server's manifest names 90 stylesheets and font files, all present in the client output.
  • On three local workspaces with Playwright: Bramble (DM Sans with Bricolage Grotesque 800), Ledger (Lato with Merriweather 700) and Juniper (Inter with Fraunces 600). Each document preloaded its one or two files as crossorigin woff2 and linked the stylesheets. The browser fetched each file once, from the head, the stylesheets came from the head rather than a script, and no preload went unused.
  • Arrival against the first paint, three loads each: with the links, the design's files arrived by the first paint in 8 of 9 loads (once 16 ms after it). With the font links stripped from the document, so the stylesheet arrives only after hydration, the other weights it names (DM Sans 500 and 600, Lato 700) arrived 300 to 500 ms after the first paint and swapped in.
  • New tests, each checked by breaking the code it covers: the loader's modules cover every bundled family with files; the manifest takes only the font modules' stylesheets and only Latin woff2 files, not latin-ext, italic or woff; the links preload the body at 400 and the headings at their weight or the nearest one, once for one family at one weight, and nothing for Inter; the design view carries them; legacy carries none.
  • The core bench holds every ceiling. Each page's scripts grow by 0.1 to 0.2 KB. Three admin pages (Labs, Developers, Office hours) take their readings as ceilings: main already reads about 2% over their old ceilings, and these bytes leave them under 0.1 KB from the limit.

A family loads with a dynamic import of its CSS module, so the client build
gives its files hashed names the server could not know, and a design's
fonts arrived only once the page's script asked: text drew in a fallback
face and swapped after hydration.

A build plugin now records each font module's stylesheet and Latin woff2
files from the client build, by the font id the loader gives it, and hands
them to the server build as a virtual module. The design view carries the
links: each family's stylesheet and at most two preloads, the body at 400
and the headings at their weight or the nearest the family has. Inter
ships with the app's own stylesheet and needs neither.

Three admin pages' script ceilings take their readings: main already reads
about 2% over them, and these bytes leave them under 0.1 KB from the limit.
@mortondev
mortondev added this pull request to stack #833 October 31, 2026 07:32
Importing the type from the build plugin gave server code a dependency on
lib/build, which the dependency graph does not allow for.
@mortondev

Copy link
Copy Markdown
Member Author

Condensed into #879. The stack is now five PRs, #878 to #882; this branch is kept as it was.

@mortondev mortondev closed this Nov 1, 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.

1 participant