All guides

Performance · Geist · HTTP/2

Why Geist Mono triggers another Layout: subsets and preload

Author: Grundy · Checked: October 25, 2026

Loading a monospace font unexpectedly invalidated the homepage heading and ordinary text. We traced the cause, tested separating rare Geist subsets and checked why Latin and Cyrillic appeared to download twice. The scope of font invalidation narrowed, but full-page LCP showed no stable improvement. These are separate laboratory studies over HTTP/2 with CPU ×4, not PageSpeed reports or a deployed font-loading fix.

1. Why loading Geist Mono invalidates ordinary text

On October 23 we isolated Mono loading: external startup JS was not downloaded, and only the Mono response received an artificial 3000 ms delay. Even after the main Geist had loaded, the Mono notification marked 119 unique nodes with Fonts changed, including H1. Contain: layout style on the example did not prevent this. Source inspection at the test Chrome's exact Blink revision explains the chain: kFontFaceLoaded is not scoped to a family, and FontFallbackMap invalidates lists with HasLoadingFallback(). Declared but unused unicode subsets can keep that flag set for the main Geist. This explains the invalidation scope; Mono does not replace the heading's font.

Delayed Mono: four runs per variant
MetricAll Geist subsetsLatin + Cyrillic only
Unique Fonts changed nodes1196
Dirty layout objects13122
H1 invalidated, runs4 / 40 / 4
Median Mono Layout, ms33.221.0
Mono Layout min–max, ms26.6–33.816.3–24.4
Median page LCP, ms2,5242,678
Median Layout over 6 s, ms897.6946.2

The table uses a separate eight-load control retaining only the used Latin and Cyrillic faces of the main Geist; Mono and all delivered font bytes stayed unchanged. H1 disappeared from the late invalidation in all four controls. A minimal document without Next.js reproduced the scope change: nine nodes became two, PRE and its text. This establishes the broad invalidation mechanism, not a general fix: deleting subsets loses coverage elsewhere. The smaller Mono pass did not improve full-page LCP or total Layout in this small series.

The CSV retains all 32 loads: 16 initial tests, eight subset controls, four final-appearance checks and four minimal-document tests. The first 16 have an error in an extra post-trace CDP font query caused by a stale node ID; their recorded traces remain valid for invalidation scope. The corrected follow-up series supplies this table. Selected fonts and final geometry match in those controls. Later checks also clarified that Mono styles a small label above H1, not just PRE; the heading itself uses Geist.

Download 32 Mono study loads as CSV

2. Separate families without losing characters

Removing rare subsets is only a diagnostic control for a particular text. On October 24 we therefore tested a version retaining full coverage: Latin and Cyrillic stayed in Geist, three rare subsets became Geist Extended, and the stack became Geist, Geist Extended, Geist Fallback. All 13 original font-face rules remain; only three family names change. The 11 WOFF2 sources, unicode-range values, weights 100–900, font-display and metrics stay unchanged. The main series executed startup JavaScript: 16 successful cold loads, eight per variant, in alternating order.

Startup JS executes: eight runs per variant
VariantMedian LCP, msLCP min–max, msMedian Layout over 6 s, ms
Original families2,7102,600–2,780921.7
Separate rare subsets2,7142,660–2,904909.8

There was no stable LCP improvement. The paired median change was +28 ms for LCP and +18.3 ms for total Layout; three of eight pairs improved LCP. A difference between group medians is not the median of paired changes. All completed slow runs remain. The separate mechanism series blocked startup JS and delayed Mono by 3000 ms: it tests invalidation scope, not normal loading speed.

Isolated Mono: four runs per variant
VariantFonts changed nodesH1 invalidated, runsMedian Mono Layout, msMono min–max, ms
Original families1194 / 432.930.3–41.4
Separate rare subsets120 / 423.018.2–32.5

Coverage checks retained 604 Geist and 762 Geist Mono code points, using the intersection of declared unicode ranges and actual cmap. In 24 browser samples, including extended Cyrillic, Latin, Vietnamese, combining marks and symbols at weights 400, 500 and 700, text, geometry and selected fonts matched. Final screenshots matched pixel for pixel. This does not establish identical intermediate frames or every possible fallback combination. The remaining 12 invalidated nodes include Mono and special-symbol text; they are not all unnecessary.

One mechanism attempt failed with Navigating frame was detached and was retried; its slow replacement (LCP 3864 ms) remains in the data. The ordinary JS series needed no retries. Cache-disabled/no-store delivery produced five font responses in both ordinary variants; the next study explains this limitation. Splitting families preserved coverage and narrowed the late pass, but the measured LCP does not justify deploying it as a speed optimization. This was a laboratory CSS transformation, not an implemented Next.js change.

Download all 24 completed family-split measurements as CSV

3. Repeated preload does not mean the font is applied twice

The family-split series received five font responses instead of three after JS startup. A separate October 24 study identified their source: Latin and Cyrillic are initially requested through the HTTP Link header; later the React Flight decoder processes two HL records and React DOM adds matching link rel=preload elements. The early HTTP hints did not create DOM elements for the client registry to find. Initiator stacks and DOM observation confirmed this path in the frozen published bundle. Adding a hint, however, does not by itself prove another network transfer.

Cache and Flight hints: four completed runs per mode
ModeLate DOM hintsFont responsesMedian LCP, msLCP min–max, ms
Cache disabled, no-store252,6442,524–2,892
Cache enabled, no-store23 or 5 (two runs each)3,0442,604–3,892
Cache enabled, immutable232,7282,496–3,816
Lab, without two Flight HL hints032,7062,452–2,936

Each mode used a fresh Chrome process and private context, the same nine startup scripts, early HTTP preload, font bytes, HTTP/2, Slow 4G and CPU ×4. Cold initial navigation does not require disabling reuse within that navigation. Only the diagnostic no-hints mode removed the two Flight HL records; this is not a proposed edit to generated site HTML. Total Layout was counted over the same six-second window. Two Chrome startup failures were retained and retried after the main series, changing the planned order; a leftover process from a failed launch further limits small CPU-time comparisons.

The extra Latin and Cyrillic transfers added 44,188 bytes (43.2 KiB) in the cache-disabled laboratory mode. Across all 16 traces, each file produced at most one RemoteFontLoaded event: downloading another preload response did not apply the CSS font again. In the lab mode the hints appeared 704–816 ms after LCP, with only 0.84–1.46 ms of Layout after 3.5 seconds. Removing them gave no stable LCP improvement: paired median −20 ms, range −276…+412 ms. One immutable run still reached LCP 3816 ms with just three responses. These transfers therefore do not explain the whole LCP delay.

A separate check of the published homepage, in a fresh private Chrome with CPU ×4, normal cache, no network emulator and no substituted resources, found three font requests/responses over HTTP/2. All used public,max-age=31536000,immutable. Both late DOM hints appeared, but no repeated transfer or page error did. Analytics endpoints were blocked. This is one production QA check, not a comparable speed benchmark or a claim about every browser. Keep the existing immutable headers and early preload; future user-loading comparisons should use a cold context with normal caching. No font-loading optimization from these three studies has been deployed.

Download all 16 completed preload measurements as CSV