Font optimization
Fonts are one of the biggest first-paint costs. Loaded badly they cause FOIT (flash of invisible text), CLS (layout shift from fallback metrics), and an extra DNS hop. The helpers in @jorvel/runtime emit the correct <link> tags and @font-face blocks so the browser can preload, swap, and avoid the double-fetch trap.
Pure-data helpers
@jorvel/ssr. You can paste their output straight into an SSR template head.Self-host: preload + @font-face
Self-hosting beats third-party CDNs for two reasons: no extra DNS lookup, and Service-Worker caching is straightforward. The preload + @font-face pair gets you swap behavior without a flash of invisible text.
import { buildFontPreloadLink, buildFontFaceCss } from '@jorvel/runtime';
const preload = buildFontPreloadLink('/fonts/inter.woff2');
// { rel: 'preload', as: 'font', href: '/fonts/inter.woff2', type: 'font/woff2', crossorigin: 'anonymous' }
const css = buildFontFaceCss([
{ family: 'Inter', src: '/fonts/inter-400.woff2', weight: 400 },
{ family: 'Inter', src: '/fonts/inter-700.woff2', weight: 700 },
{ family: 'IBM Plex Mono', src: '/fonts/plex-400.woff2', weight: 400, unicodeRange: 'U+0000-00FF' },
]);
// @font-face { font-family: "Inter"; src: url("/fonts/inter-400.woff2") format("woff2"); font-display: swap; font-weight: 400; }
// ...Always crossorigin=anonymous
@font-face request use the same CORS mode. The helper hard-codes crossorigin="anonymous" for that reason. If your font CDN does not send Access-Control-Allow-Origin: *, the browser will fetch the file twice and the preload becomes a regression.Wiring into an SSR head
Combine the two helpers in the document head. Put the preload link before the stylesheet so the browser starts the request as early as possible.
import { buildFontPreloadLink, buildFontFaceCss } from '@jorvel/runtime';
const PRELOADS = [
buildFontPreloadLink('/fonts/inter-400.woff2'),
buildFontPreloadLink('/fonts/inter-700.woff2'),
];
const FACE_CSS = buildFontFaceCss([
{ family: 'Inter', src: '/fonts/inter-400.woff2', weight: 400 },
{ family: 'Inter', src: '/fonts/inter-700.woff2', weight: 700 },
]);
export function Head() {
return (
<head>
{PRELOADS.map((link) => (
<link key={link.href} {...link} />
))}
<style dangerouslySetInnerHTML={{ __html: FACE_CSS }} />
</head>
);
}Google Fonts
The composer URL-encodes weight / italic axes correctly and bakes in display=swap. Pair with the preconnect helper so the DNS + TLS handshake to fonts.gstatic.com happens in parallel with HTML parsing.
import { googleFontsUrl, googleFontsPreconnectLinks } from '@jorvel/runtime';
googleFontsUrl({
families: [
{ family: 'Inter', weights: [400, 700] },
{ family: 'IBM Plex Mono', weights: [{ italic: false, weight: 400 }, { italic: true, weight: 400 }] },
],
});
// https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=IBM+Plex+Mono:ital,wght@0,400;1,400&display=swap
googleFontsPreconnectLinks();
// [
// { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
// { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: 'anonymous' },
// ]Variable fonts
Variable fonts ship every weight in one file. Pass a single srcand let Rspack's asset module handle the URL hash. The weight field becomes a range like 100 900:
buildFontFaceCss([
{ family: 'Inter', src: '/fonts/inter-variable.woff2', weight: '100 900' },
]);Performance checklist
- Subset before shipping. Use
pyftsubsetto strip glyph ranges your product never uses — typically Latin Extended cuts file size by 60-80%. - Two weights max in the critical path. Each additional weight is another file the browser blocks on. Defer non-critical weights with
font-display: optional. - Preload only what renders above the fold. Preloading every weight ratchets up bandwidth without improving LCP.
- Use
size-adjustfor the fallback. Pair the web font with a system fallback (system-ui) tuned to similar x-height to minimize CLS during the swap window. - WOFF2 only. WOFF and TTF/OTF are 30-50% larger and only used by IE11/Android 4. The helper does not emit fallbacks for those formats by default.
CSP impact
Self-hosted fonts need font-src 'self'. Google Fonts needs font-src https://fonts.gstatic.com and style-src https://fonts.googleapis.com. The buildCsp helper in @jorvel/security can take a list of allowed font origins; otherwise the browser blocks the request with no console error.
