Web fonts break layout: where exactly the shift occurs
Layout shift from web fonts is almost never related to the font file itself — it occurs at the moment of font substitution.
While the browser is downloading the typeface, it either hides the text (invisible text) or immediately renders it with a system fallback, and then repaints it with the loaded one.
These two typefaces have different character widths, line heights, and metrics, so a paragraph laid out for one gets reflowed into a different number of lines when the font is substituted.
Then a chain reaction kicks in. A line that was one becomes two — all content below shifts down. Buttons, form fields, fixed-height blocks, and sticky navigation jump by dozens of pixels, and by that moment the user has already started reading or aiming at an element.
The layout shift metric records each such displacement as a separate event, and it is font substitution that accounts for the lion's share of these events on a typical page.
A separate case is invisible text. If the hiding period is set too long, the user sees empty blocks instead of a heading, and then a sharp jump of lines when the typeface finally loads.
This is worse than an instant display with a fallback font: the page looks broken exactly at the moment when the visitor is deciding whether to stay on it.
The root of the problem is a desynchronization: the browser does not know the metrics of a typeface that has not yet loaded and cannot reserve space in advance.
This means it needs to be addressed at the level of configuring loading and describing metrics, rather than by picking a "similar" font by eye. Let's break down how this works and what exactly gets configured.
What controlled font loading delivers
Controlled font loading is when we decide in advance what the browser will show in the first milliseconds and how it will swap the typeface once the file finishes loading.
Without this decision, the layout "breathes": text is rendered in a system font, then swapped for a web font with different metrics, and the entire block below jumps. Below is exactly what changes when loading is managed deliberately.
- Stable layout. We match the fallback typeface metrics to the target one — character width, line height, and baseline position all align. The swap happens without shifting blocks, and the layout shift score drops to zero.
- Predictable first-screen rendering. Critical typefaces are declared in advance and loaded with priority, while the rest load as needed. The first screen renders in the same way on every visit, not "however it turns out."
- No text jumps. Each typeface has its own swap period: short enough to avoid a flash of the system font, but long enough not to block rendering. Text doesn't reflow mid-read.
- Resilience on slow connections. If loading fails or times out, the browser stays on the fallback typeface with the same metrics — the page remains readable and the layout doesn't fall apart.
- Predictability across the entire device fleet. We test behavior at different resolutions, languages, and data-saving modes, not just on our own laptop. The result is consistent text behavior for all visitors.
These points are verifiable: before and after, we measure layout shift and time to first text render, and the difference shows up in numbers, not in words.
Ways to Load Fonts: Comparing Approaches
The choice of how to load web fonts determines how many kilobytes of the critical path the browser will load before the first text render, and whether layout shift will occur.
Below are four working options that teams actually choose between on projects, with their strengths and limitations.
| Option | When it fits | Limitations |
|---|---|---|
| Self-hosting | Branded font, stable traffic, privacy requirements | You need to prepare the character subset yourself, monitor file updates and cache headers |
| External service | Quick start, many languages and weights, no time to prepare files | Dependency on a third-party domain: an extra DNS connection, risk of blocking, license restrictions on self-hosting |
| System stack | Interfaces, admin panels, content blocks where only the text matters | No control over appearance: the typeface differs across platforms, branded typography is lost |
| Character subset | Cyrillic + Latin, one or two alphabets, large volumes of text | Requires splitting into ranges and checking that rare characters do not fall back to substitution |
Self-hosting gives full control: files live on your own domain, are versioned alongside the release, and font-display: swap plus preloading the key weight eliminate invisible text at startup.
The limitation is maintenance: when the typeface is updated, you need to rebuild the character subset and reset the cache, otherwise some users will remain on the old version.
An external service removes file preparation and serves the required range for the browser itself, but adds an external domain to the critical path.
If the service is unavailable or blocked, the page will render with a system font — this is acceptable as a fallback, but not as the primary option.
The system stack loads nothing: zero weight and zero layout shift. It makes sense to keep it as the final fallback in font-family for any of the first three approaches — then if loading fails, the text is still readable immediately.
How we set up font loading: stages of work
We start with an audit: we look at where web fonts are actually loaded from — your own files, external catalogs, connections via third-party services.
Often a single page accumulates 4-6 sources, some of them are duplicated, and some pull in extra weights that aren't used in the layout. At this step we record which typefaces the design needs and which are left over "for memory" from previous iterations.
Next, we build a priority scheme: weights critical for the first screen load earlier, decorative ones — after the main content. Below is what the process looks like as a whole.
- Audit of connections. We collect all font sources on the pages, compare them with the mockup, and identify extra weights, duplicates, and external dependencies.
- Priority scheme. We determine which typefaces are needed for the first screen and which can be loaded deferred, and we fix this in the rules.
- Loading setup. We specify formats, preloading of critical files, and fallback options so that the text doesn't "jump" when the font is swapped.
- Space reservation. We set the metrics of the fallback font so that blocks occupy their final size before loading — this is what eliminates layout shift.
- Testing under real conditions. We run the pages on a slow connection and mobile devices, measure layout shift and text rendering speed.
- Adjustments based on results. If the shift remains, we correct the metrics or the set of weights and repeat the measurement.
- Handover. We deliver the configuration, connection rules, and a short instruction on how to add new typefaces without regression.
After handover, the client is left with a reproducible scheme: any new font is connected by the same rules, not "however it turns out." Describe your task — we'll come back with an estimate and a work plan.
What the client receives
The package is assembled so that the font part can be handed off to any engineer or contractor without losing the page's behavior. Below is exactly what goes into the repository and the documentation.
- Connection configurations: font files in WOFF2,
@font-facewithfont-display: swap,unicode-rangeby character sets, and preload only for the first-screen weights. - Substitution rules: a
font-familychain with system fallbacks,size-adjustandascent-overridemetrics tuned to the primary font — the substitute does not shift lines. - Character sets: separation into Latin, Cyrillic, and service symbols so the browser does not pull extra weight on pages that do not use them.
- Loading scheme: preload order, cache headers for font files, behavior on a slow network and when web fonts are disabled.
- Documentation: which weights are used, where the sources are stored, how to check layout shift after edits.
- Regulation for adding new weights: who prepares the file, which checks the weight and metrics pass, how the configuration is updated, and who approves the rollout.
- Acceptance checklist: measuring shift before and after, testing on mobile and in data-saving mode.
Such a package removes dependence on a single person: any engineer on the team can make an edit, and the behavior of text on the page remains predictable.
Case study: eliminating layout shift on a services portal
The services portal is a catalog with a personal account area where users compare plans and submit requests. The font was loaded through an external service: five weights, the full glyph set, including Cyrillic and symbols that never appeared on the pages.
The browser fetched the files after parsing the markup, and until they arrived the text was re-rendered in a fallback weight with different metrics.
Measurements before the fixes: layout shift on plan cards and in the account header, time to readable text — a noticeable pause on mobile networks. Worst of all were slow connections: the user would start reading and the lines would "jump" under the cursor.
What was changed
The weights were split by purpose: two remained for body text and headings, the rest were removed. Glyph subsets were built for the portal's actual languages — extra symbols were stripped out.
The primary weight was preloaded with high priority, the rest were left to the browser with font-display: swap.
The key part is metrics. For the fallback weight, size-adjust, ascent-override, and descent-override were set via @font-face so that the line height matched the target font. In addition, space was reserved for blocks with large text.
What the result was
Layout shift on key screens is gone — the metrics match, and re-rendering no longer moves blocks. Time to readable text dropped: the fallback weight is shown immediately and does not change the geometry.
The number of loaded weights fell by more than half, and the font weight on first load dropped severalfold.
A rule remains in place for support: a new weight is added only after measuring metrics and testing on mobile. The approach is being carried over to other projects — web fonts stop being a source of layout shift when the fallback's metrics are set in advance.
How preloading differs from deferred font loading
Preloading is when the browser is instructed to download a font file first, even before the parser reaches the rules where that font is declared.
From an engineering standpoint, it looks like this: at the beginning of the document there is a link with the rel="preload" attribute, the resource type font, and a mandatory crossorigin — without the latter, the browser will download the file twice, because fonts are always requested in anonymous CORS mode.
This mode is justified for one or two weights used for a heading and the first screen: while the markup is being parsed, the file is already in the cache, and the critical text is rendered immediately in the right typeface, without a system font substitution.
Deferred loading works the opposite way: the font is declared in the rules, but is connected non-blocking. The browser first renders the text in a system weight, and when the file finishes loading, it replaces it.
This is where the shift is born: the metrics of the system and target fonts differ, the line changes width, and the entire block below it jumps down.
This is fixed not by abandoning deferred loading, but by adjusting the metrics — the size-adjust property and overriding ascent-override in the @font-face rule, so that the fallback font occupies exactly the same area. Then the substitution happens without layout shift.
The choice between modes is a choice between the time to first text render and layout stability.
Preloading speeds up the appearance of the right typeface, but competes for bandwidth with critical resources: if you preload five weights, they will take bandwidth away from styles and scripts, and the first screen will appear later.
Deferred loading does not slow down rendering, but requires that the fallback weight be metrically compatible with the target one.
In practice, the modes are combined. The main weight for the first screen goes through preloading, the rest — deferred, with font-display: swap and matched fallback font metrics.
Additionally, fonts are served in the woff2 format and hashed in the file name so that the cache does not serve the old version after the typeface is updated.
The result — critical text is visible immediately, and the shift stays within limits that neither the user nor the layout stability metric notices.
Common technical concerns
Fonts rarely exist in a vacuum: usually there's an external service, several languages, and rare characters like em dashes, curly quotes, or currency symbols. Below are answers to what we encounter most often.
We already have an external font service connected — will we have to redo everything?
No. An external service serves files and styles, but it doesn't control how the browser displays them. We keep the connection, add font-display: swap and metric fallbacks — a temporary system font with tuned size-adjust and ascent-override so the swap doesn't shift the text.
Then we decide case by case: if the service is slow on the first screen, we move the critical weights in-house and serve them with a long cache, leaving the rest as is.
What about multiple languages and rare characters?
We split the set by character ranges: Latin, Cyrillic, Hebrew, punctuation — each in its own file with unicode-range.
The browser pulls only the ranges that actually appear on the page, so an English-speaking visitor doesn't load Cyrillic, and a rare character doesn't drag the whole font along with it.
For characters not in the main set, we set an explicit fallback in font-family so a system glyph is substituted instead of a "tofu box".
What happens if the font doesn't load at all?
The page stays readable: the text shows in a system font, and the layout doesn't break thanks to the metric fallbacks. This is a deliberate choice in favor of accessibility — the visitor sees the content immediately, and the typeface is substituted as it loads.
We test the scenario by disabling the network in the developer tools and compare the layout shift before and after the swap.
How hard is it to maintain going forward?
The rules are described once in the configuration: which weights, which ranges, which cache. After that it's part of the build — when adding a language or a character, it's enough to extend the range rather than rewrite the styles. We record the metrics before and after so that the layout shift stays at zero with every update.
Let's discuss your font challenge
To come back with an estimate and a plan rather than generalities, we need input on your project.
Font topics almost always come down to specifics: which typefaces are already connected, where they load from, and exactly where the layout "jumps." The more precise the initial data, the fewer iterations for approval.
What to send for an estimate:
- a list of typefaces and weights currently in use, indicating where they are hosted;
- a link to the live site or staging environment where the content shift issue reproduces;
- screenshots or a recording of shift metrics from the developer panel on a slow connection;
- data on which browsers and devices are the priority for your audience;
- design requirements: which weights are critical, which can be replaced with system fonts;
- hosting, CDN, and caching policy constraints, if any;
- the contact of the person who makes decisions on layout and design.
As a deliverable, you will receive an implementation plan: which typefaces we keep, which we move to local hosting, how we set up preloading and metric overrides, and which checks we add to the build. Plus a timeline estimate by stages and a list of changes that need to be made on the design side.
Describe your task — we'll come back with an estimate and a plan.







