Measure and Reduce Layout Shift From Web Font Substitution
A deterministic system-font model isolates the geometry change caused by swapping fonts with different metrics, then compares it with making the final face available before the first measured layout.
fallback -> final- Viewport height
- --
- Text before / after
- --
- Target movement
- --
final before layout- Viewport height
- --
- Text before / after
- --
- Target movement
- --
Three-browser evidence verified. Live values come from your current browser. The formal matrix records the controlled browser runs and fixture revision used for the published conclusion.
Observed failure
The final font changes the heading's line count
The failing fixture begins with Courier New and then applies Georgia to a fixed project-authored heading. At narrow widths, the final face occupies fewer lines and moves the following status panel upward.
Minimal code diff
Stabilize font metrics before the first layout
The fixed fixture begins with the final face already available, so the same mutation does not change line wrapping. In production, load critical fonts early or metric-match the fallback with accurate font-face overrides.
Before
font-family: 'Courier New', monospace;
/* later swaps to Georgia */After
font-family: Georgia, serif;
/* final face available before layout */Root cause
Fallback and final fonts can have different glyph widths, ascent, descent, and line-gap metrics. A heading that fits in three lines with one face may use four with another. When the browser swaps faces after initial layout, everything after that heading can move.
This fixture deliberately isolates metric substitution from networking. The failing state changes from Courier New to Georgia after the first measurement. The fixed state starts and ends with Georgia, representing a critical final face available before first layout. It is a repeatable geometry test, not a simulation of a specific connection or font provider.
Production choices
Early loading is one option, but it is not the only one. For a real web font, measure the actual final and fallback files. Depending on the result, use an appropriate preload and delivery strategy, choose a closer fallback, or define tested size-adjust, ascent-override, descent-override, and line-gap-override values. Do not copy percentages from an unrelated font pair.
Measurement support
Every browser run records the heading height before and after substitution plus the following panel’s vertical movement. Chromium additionally records non-input layout-shift performance entries. Firefox and WebKit results do not claim Layout Instability API support.
When this fix is not enough
Making every font render-blocking can delay text and harm other performance metrics. Metric overrides can also be wrong for a different weight, script, variable-font axis, or platform font version. Test the exact production files, text languages, weights, and browser set.
Regression condition
At 320 and 375 CSS pixels, the failing substitution must change heading height and move the following panel by a positive amount. The fixed state must leave both the heading height and target position unchanged. Both states must have zero page-level horizontal overflow.
Primary references
Sources checked for this case
- CSS Fonts Module Level 5 - font metric overrides - W3C, checked 2026-08-04
- Best practices for fonts - Google web.dev, checked 2026-08-04
- Optimize Cumulative Layout Shift - web fonts - Google web.dev, checked 2026-08-06
Formal evidence
Browser and viewport matrix
Cells show failing / fixed target movement in CSS pixels.
| Browser | 320 | 375 | 768 | 1024 | 1440 |
|---|---|---|---|---|---|
| Chromium | +29 / +0 | +29 / +0 | +0 / +0 | +0 / +0 | +0 / +0 |
| Firefox | +29 / +0 | +29 / +0 | +0 / +0 | +0 / +0 | +0 / +0 |
| WebKit | +29 / +0 | +29 / +0 | +0 / +0 | +0 / +0 | +0 / +0 |
20/20 recorded assertions passed
chromium-failing-movement-32029pxchromium-fixed-movement-3200pxchromium-horizontal-reflow-3200px / 0pxchromium-layout-instability-api-3200.005858153343200684 / 0chromium-failing-movement-37529pxchromium-fixed-movement-3750pxchromium-horizontal-reflow-3750px / 0pxchromium-layout-instability-api-3750.004772667261445473 / 0firefox-failing-movement-32029pxfirefox-fixed-movement-3200pxfirefox-horizontal-reflow-3200px / 0pxfirefox-failing-movement-37529pxfirefox-fixed-movement-3750pxfirefox-horizontal-reflow-3750px / 0pxwebkit-failing-movement-32029pxwebkit-fixed-movement-3200pxwebkit-horizontal-reflow-3200px / 0pxwebkit-failing-movement-37529pxwebkit-fixed-movement-3750pxwebkit-horizontal-reflow-3750px / 0px
Run 2026-08-04T11-36-57-234Z. chromium 149.0.7827.201, firefox 153.0, webkit 26.5. Open raw manifest