Preloading a font changes when it arrives; it does nothing about what happens when it arrives. font-display: swap tells the browser to render text immediately in a fallback font and then swap to the webfont once it loads. If the webfont and the fallback font don’t share the same metrics (x-height, character width, ascent, descent, line gap), that swap physically reflows the text: lines wrap differently, elements around the text block move, and the browser reports a layout shift. Preloading only removes the network-latency portion of the problem, it gets the webfont to arrive sooner so the swap happens earlier in the page’s life. It does not touch the metric mismatch that causes the reflow in the first place, so a preloaded font using swap can still produce a CLS spike, just an earlier one.
Mechanism
Under font-display: swap, the browser follows a defined timeline: during a short block period it renders nothing (or an invisible placeholder) for text using that font, then during the swap period it uses a fallback font and will swap to the real webfont as soon as it finishes downloading, for as long as the font-loading timeout allows. The entire point of “swap” as a strategy is to prioritize showing text quickly over showing the exact intended font quickly, which is generally the right tradeoff for perceived performance and for avoiding invisible text. But whichever fallback font renders during that window has its own default metrics, typically a system font like Arial, Helvetica, or a generic serif/sans-serif, and those metrics are essentially never identical to a custom webfont’s metrics. Different x-heights and character widths mean a paragraph set in the fallback occupies a different amount of horizontal and vertical space than the same paragraph set in the webfont. When the swap happens, every line affected reflows, and anything positioned relative to that text (elements below it in normal flow, siblings depending on its height) shifts position. Chrome’s layout instability metric records that as a layout shift with a non-zero impact fraction, and if the moved region is large enough or the distance is far enough, it can push CLS for the page into the “needs improvement” or “poor” bracket on its own, from a single swap event.
Preloading a font with <link rel="preload" as="font" crossorigin> changes the fetch priority and start time of the download, so the font is more likely to already be available in cache by the time layout would otherwise trigger the swap, potentially shrinking or effectively eliminating the swap period’s duration. That’s a genuine improvement for LCP and for perceived text flash duration. But it has no effect on the metric mismatch. Whether the swap happens at 50ms or 400ms into the page load, if the two fonts have different metrics, the shift still happens when the swap occurs. Preloading can even make the problem more visible in some cases, because a very fast preload means the swap happens while the user is still looking at and reading the initial layout, rather than after they’ve scrolled away from it.
Practical implication
The actual fix operates on the CSS Fonts Module’s font metric override descriptors, not on loading timing. Define a fallback @font-face rule that points to a locally available system font (using src: local(...)) but overrides its reported metrics to match the webfont’s real metrics, using size-adjust, ascent-override, descent-override, and line-gap-override. When the fallback’s reported metrics match the webfont’s actual metrics closely enough, the swap produces little or no line-wrap change and little or no shift, because the space the text occupies barely changes between the fallback render and the webfont render. Tools exist to calculate these override values automatically from a given webfont file (comparing its metrics against common system fallbacks), and several build tools and frameworks now generate this fallback @font-face block automatically as part of font optimization.
A minimal version of the pattern looks like this:
@font-face {
font-family: "MyWebfont";
src: url("/fonts/mywebfont.woff2") format("woff2");
font-display: swap;
}
@font-face {
font-family: "MyWebfont Fallback";
src: local("Arial");
size-adjust: 104%;
ascent-override: 96%;
descent-override: 24%;
line-gap-override: 0%;
}
body {
font-family: "MyWebfont", "MyWebfont Fallback", sans-serif;
}
The percentages are specific to whichever webfont and fallback pairing is in use; they come from comparing the two fonts’ actual metrics (either calculated by hand from the font files’ metadata or generated by an automated fallback-matching tool), not from a generic default. The fallback’s job here is purely geometric: it never needs to look identical to the webfont, it only needs to occupy the same horizontal and vertical space so that the eventual swap doesn’t move anything else on the page. Once that geometric match is close enough, the swap can still be visually noticeable (a reader may see the letterforms change) without being a layout-shifting event, which is the actual target, since CLS measures movement, not the visual identity of the text itself.
A second valid approach is font-display: optional. This tells the browser to use the fallback for the initial render and only use the webfont if it was already available (from cache or a fast enough fetch) before the browser needed to start painting text, otherwise it keeps the fallback for that page view entirely rather than swapping later. This trades “guaranteed correct branding font” for “guaranteed no layout shift from font swap,” which is often the right tradeoff on pages where CLS matters more than exact typography on every visit; combined with preloading, optional gives the webfont a real chance to win the race on repeat views (once cached) while never causing a mid-session shift.
In short: preload to fix timing, use metric overrides (or font-display: optional) to fix the shift itself. Doing only the first, which is the common mistake, leaves the CLS spike intact because it addresses a different part of the problem than the one causing the score to fail.