On 17 August 2026 we were running an SEO audit of our own website. Speed-wise, the homepage looked fine: on a 1920×1080 desktop the first content appeared in 820 ms. But the same line of the report showed a CLS of 0.2283, that is, the cumulative layout shift of the first screen. The mobile run (390×844) on the same day showed 0.0000.
A reminder of the thresholds: up to 0.1 counts as “good,” above 0.25 as “poor.” So 0.2283 formally only “needs improvement,” but it was very close to the poor zone. Repeat runs within the same audit gave 0.2284, so it was no coincidence.
Below we explain how we looked for the cause, which common tips measurably did not help, and how to check the same thing on your own site. First, the limits: all lab figures were taken on the homepage of our own website in Chromium at 1920×1080. On a different template and with different fonts the numbers will differ, but the search method will still work. The audit took the starting 0.2283 on the live site over the network, while 0.0056 and the 3 September measurements were taken on a local production build, with no network latency to the site.
What exactly was jumping
The audit recorded a single jump at 739–777 ms after the start of loading. The right-hand block of the first screen, holding the heading and a paragraph, shrank in height from 507 to 468 px. Everything below it moved up 39 px along with it.
The cause of the jump was a font swap. The page is first drawn in a fallback system font, and once the brand font finishes loading, the browser redraws the text (font-display: swap). These fonts have different metrics: line height, letter width. Because of this, the text takes up a different amount of space, and neighboring blocks shift.
Why nobody noticed
The first reason: there was no shift on phones, and almost everyone checks the homepage on a phone.
The second reason is field data. For the 28 days up to 7 September, PostHog showed a p75 CLS of exactly 0 both on mobile (40 measurements) and on desktop (24 measurements). Only visitors who accepted cookies were counted. The sample is small, and most of that window falls before the fixes. In other words, the problem simply did not show up in the field data. Our assumption (we did not measure it separately): on a repeat visit the font is already in the browser cache and arrives before the first paint, so there is no swap.
The third reason: the jump lasts a fraction of a second. A developer with a “warm” cache does not see it.
Step 1: fallback font metrics (17.08)
The same day we did the obvious thing: added a fallback @font-face with vertical metrics taken from the font file itself rather than from memory (ascent-override, descent-override, line-gap-override).
A repeat frame-by-frame measurement showed this was not enough: the first-screen block still shrank from 511 to 468 px. We had aligned the line height, but the jump barely changed.
Step 2: the ch unit (03.09)
The second cause was found in a single class. The paragraph had a width limit of max-width: 40ch. The ch unit is the width of the “0” glyph in the font that is active right now. So while the text is set in the fallback font, the width is one thing, and after the swap it is another.
Here is what the frame-by-frame measurement showed:
| Moment | max-width | Paragraph height | Lines |
|---|---|---|---|
| 915 ms | 360 px | 112.2 px | 4 |
| 938 ms | 400 px | 84.1 px | 3 |
The container became 40 px wider, the paragraph lost a line, and the block slid. We replaced 40ch with a fixed 400px: in the brand font this is the same width, so the line length did not change for the reader. After that, the block went from 483 to 468 px instead of from 511. The remaining CLS was 0.0640, already in the green zone. Still, we wanted to understand what was holding even that remainder.
Step 3: the skewed decorative band
The remainder came from a decorative band with outlined words, skewed via skew. For a skewed element, the height of the bounding box equals the line height plus the word width multiplied by the tangent of the angle. So any change in text width turns into a change in height. During the font swap, the three words of the band changed height like this: 207→212, 203→199 and 179→186 px. By shift area, the band outweighed all the text on the first screen.
We also ran a control experiment: we blocked the font files completely. CLS became exactly 0.0000, so the shift was 100% caused by the font, and there was nothing else to look for.
Next we tested the standard tips. Each one was checked by measurement, not by eye:
| What we tried | CLS |
|---|---|
size-adjust in the fallback font |
0.0640 — no change |
| line height in pixels | 0.0640 — no change |
font-display: block |
0.0930 — worse |
font-display: optional + preload |
0.0056 |
size-adjust aligns the average line width, but on specific uppercase words the error remains. Line height in pixels does not help, because the skew adds width back into height. block gave the most interesting result: while the font loads, the text is invisible, but the browser calculates its space using the fallback font. When the font arrives, the swap happens anyway.
font-display: optional worked. The browser waits for the font for about 100 ms, and if it has not arrived, the fallback stays until the page finishes loading, with no swap. But on its own it would have ruined the design: the font almost never arrived in time, and the band would have been drawn in the fallback Arial. The measured band width was then 839 px instead of the brand 753. So we added <link rel="preload"> for the font file so it starts loading together with the HTML rather than after the CSS is parsed. With that, the band measurably has the brand 753 px, and there is no shift.
The final measurement: three runs, a local production build, 3 September. On desktop, CLS 0.0056 with LCP 876 ms; on mobile, CLS 0.0000 with LCP 768 ms.
We applied optional only to the decorative band. You cannot do this for body text: the reader would see one font or the other depending on network speed.
What the fix cost and what we found along the way (07.09)
The preload turned out not to be free. 4 days later, PageSpeed on mobile named this very band as the largest element of the first screen (LCP). And its font, a full 22 KB file, was being preloaded on every page of the site. The band needs only 17 glyphs, so we cut a 2.8 KB subset out of that same file.
During the same investigation, the network requests revealed two more fonts, 58 and 57 KB. They loaded on every page, although no style on the site used them: the package remained connected in the root layout, even though nobody read its variables. We removed the import and checked that the text looked the same.
How to check your own site in 15 minutes
You will need Chrome and a desktop window. Check mobile mode separately: in our case the problem was present on only one of the two.
- Open DevTools, the Network tab, and turn on Disable cache. This way you see the page through the eyes of a new visitor.
- Go to Performance and record a reload. The Layout shifts track will show every shift and the elements that moved. You can also get by with the console by pasting this after the page loads:
new PerformanceObserver(list => {
for (const e of list.getEntries()) {
if (!e.hadRecentInput) console.log(e.value.toFixed(4), e.sources.map(s => s.node));
}
}).observe({ type: 'layout-shift', buffered: true });
- Run a control experiment. In Network, right-click the font file → Block request URL (or set the pattern
*.woff2in Request blocking) and reload the page. If the shift is gone, the font is to blame, and you can leave images and banners alone. - Set the Font filter in Network and list all the font files. Then, in Elements → Computed, look at Rendered Fonts on several text blocks. A file that is not among the fonts actually used is a candidate for removal.
- Search the first-screen styles for the
chunit in widths, as well asskeworrotateon blocks with text. Both turn a font change into a size change.
We only measured our own Next.js site with self-hosted fonts. In an OpenCart theme with Google Fonts the swap mechanism is the same, but we have no figures for that case.
What these figures do not mean
- 0.0056 is a lab measurement on a local build, not a promise for every visitor. Network, cache and device change the picture.
- We cannot credit the zero in the field data to the fixes: most of the window falls before them.
- On 12 September the site’s main fonts changed. The fallback metrics for the new fonts were calculated with the same method, but we did not repeat the frame-by-frame measurement of the first screen for this article. All the figures above refer to the site before that change.
- CLS is one of the Core Web Vitals metrics. A stable first screen removes a technical defect; it does not raise rankings by itself.
How we handle the other metrics is described in the section on Core Web Vitals, and servers and page weight are covered in the article on website speed for business. If the first screen on your site jumps and the cause is not obvious, take a look at how we approach website speed optimization.




