Web Performance

防止字体加载导致的布局偏移(CLS)

Updated 二月 24, 2026
字体加载是累积布局偏移的隐藏来源。学习size-adjust、字体度量覆盖以及消除字体相关CLS的策略。

Preventing Layout Shift (CLS) from Font Loading

Cumulative Layout Shift measures visual instability — the degree to which page elements move unexpectedly after initial render. A score below 0.1 is considered "Good" by Google's Core Web Vitals standards. A score above 0.25 is "Poor" and affects search ranking.

Fonts are one of the most consistent sources of poor CLS scores. When a browser renders text with a fallback system font and then swaps in a custom typeface with different metrics, paragraphs reflow. Text blocks shrink or expand. Elements below the text jump. On pages with large text blocks, a single font swap can push CLS from 0.02 to 0.3 — entirely in the "Poor" range.

This guide explains why font swapping causes layout shift and walks through every technique for eliminating or minimizing it.


How Fonts Cause Layout Shift

Every typeface has a set of typographic metrics that define how it occupies space:

  • Ascent: The distance from the baseline to the top of the tallest character
  • Descent: The distance from the baseline to the bottom of descending characters (g, y, p)
  • Line gap: Additional spacing added between lines of text
  • Advance width: The horizontal space each character occupies
  • x-height: The height of lowercase letters relative to capitals

When a fallback font (Arial, Helvetica, Times New Roman) has different metrics than the custom font (Inter, Roboto, Lato), text occupies different amounts of vertical and horizontal space. A paragraph that takes 5 lines in Arial might take 4.5 lines in Inter — or 5.5 lines. When the custom font swaps in, the paragraph expands or contracts, pushing every element below it up or down.

The amount of shift depends on how different the two fonts' metrics are. Some fonts have very similar metrics — Inter and Helvetica are reasonably close. Others are dramatically different — a condensed display font might be 30% narrower than the fallback, causing entire text blocks to reflow extensively.

The timing also matters. A font that loads before the initial paint causes no CLS (there's never a fallback state). A font that loads 200ms after first paint causes modest CLS because little user-initiated scrolling has happened. A font that loads 3 seconds after first paint — during active reading — causes CLS that users directly experience as content jumping beneath their eyes.


Before fixing CLS, you need to confirm that fonts are the source and quantify the magnitude.

Chrome DevTools Layout Shift Regions

In Chrome DevTools, enable the "Layout Shift Regions" overlay: Open DevTools → More tools → Rendering → Layout Shift Regions. This highlights every element that shifts in blue when it moves. Load your page and watch for font-swap events — the blue overlays will appear precisely when the custom font loads.

Performance Panel

Record a page load in the Performance panel. In the Experience track, red "Layout Shift" markers appear at each shift event. Click on any marker to see which elements shifted and by how much. The "Sources" section shows the specific DOM nodes that moved, helping you identify which font swap caused each shift.

CLS Breakdown in Lighthouse

Lighthouse reports the CLS score and identifies contributing elements. When fonts are the cause, you'll see text containers listed as shifting elements, often with timestamps matching your font load times from the Network panel.

Web Vitals Extension

The Web Vitals Chrome extension shows a real-time CLS counter and highlights shifting elements in red at the moment of shift. This is the fastest way to visually confirm font-related CLS on any page.


CSS size-adjust for Metric Matching

size-adjust is a CSS descriptor added to @font-face declarations to scale a font's metrics without changing its visual size. Applied to a fallback font declaration, it scales the fallback font to match the custom font's dimensions — so when the custom font swaps in, the text occupies the same space.

/* Custom font */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

/* Fallback with size-adjust to match Inter's metrics */
@font-face {
  font-family: 'Inter-fallback';
  src: local('Arial');
  size-adjust: 107%;
  font-weight: 400;
}

body {
  font-family: 'Inter', 'Inter-fallback', Arial, sans-serif;
}

With this setup, Inter-fallback is a modified version of Arial scaled to 107% of its normal size — close to Inter's proportions. When Inter loads and swaps in, the text dimensions are nearly identical, and the layout barely shifts.

The size-adjust percentage isn't a guess — it's calculated from the ratio of the custom font's average character width to the fallback font's average character width. For Inter vs Arial, this ratio is approximately 107%. Different font pairs have different ratios.

Calculating the Right size-adjust Value

The formula for size-adjust is:

size-adjust = (custom_font_advance_width / fallback_font_advance_width) × 100%

In practice, you measure this by rendering a string in both fonts at the same font-size and comparing the rendered widths. A quick JavaScript approach:

function measureFontWidth(fontFamily, text = 'abcdefghijklmnopqrstuvwxyz') {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  ctx.font = `16px ${fontFamily}`;
  return ctx.measureText(text).width;
}

const customWidth = measureFontWidth('Inter');
const fallbackWidth = measureFontWidth('Arial');
const sizeAdjust = (customWidth / fallbackWidth * 100).toFixed(2);
console.log(`size-adjust: ${sizeAdjust}%`);

Run this on a page where Inter is loaded and you have an accurate ratio. A more sophisticated approach measures multiple character sets and weights to produce a more representative average.


ascent-override, descent-override Descriptors

size-adjust addresses horizontal metrics (character width), but vertical metrics also affect layout. Different fonts have different ascent, descent, and line-gap values, which affect line height and the vertical space paragraphs occupy.

Three additional @font-face descriptors override these vertical metrics:

  • ascent-override: Overrides the font's ascent metric
  • descent-override: Overrides the font's descent metric
  • line-gap-override: Overrides the font's line gap metric
@font-face {
  font-family: 'Inter-fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
  font-weight: 400;
}

These values work together to make the fallback font render text blocks with the same overall dimensions as Inter. The paragraph takes the same number of lines. The line breaks occur in the same places. When Inter swaps in, the layout doesn't shift.

Getting Accurate Override Values

Calculating ascent, descent, and line-gap overrides manually is complex. The values depend on the font's internal metric tables (OS/2 and hhea tables in OpenType). Several approaches simplify this:

Using fonttools to inspect a font's metrics:

pip install fonttools
python3 -c "
from fontTools.ttLib import TTFont
font = TTFont('inter-regular.woff2')
os2 = font['OS/2']
head = font['head']
units_per_em = head.unitsPerEm
print(f'ascent: {os2.sTypoAscender}')
print(f'descent: {os2.sTypoDescender}')
print(f'line_gap: {os2.sTypoLineGap}')
print(f'units_per_em: {units_per_em}')
print(f'ascent_override: {(os2.sTypoAscender / units_per_em * 100):.2f}%')
print(f'descent_override: {(abs(os2.sTypoDescender) / units_per_em * 100):.2f}%')
print(f'line_gap_override: {(os2.sTypoLineGap / units_per_em * 100):.2f}%')
"

Apply these percentages as the override values in your fallback @font-face rule. The result is a fallback font that closely matches the vertical rhythm of your custom font, making swaps nearly invisible.


Automated Tools for Fallback Font Matching

Manually calculating size-adjust, ascent-override, descent-override, and line-gap-override for every font pair is tedious. Several tools automate this process.

fontaine (npm package)

fontaine analyzes a custom font and generates CSS overrides that minimize the metric difference between the custom font and common fallback fonts:

npm install -g fontaine
fontaine --font ./inter-regular.woff2 --fallback Arial

Output:

@font-face {
  font-family: 'Inter Fallback: Arial';
  src: local('Arial');
  size-adjust: 107.04%;
  ascent-override: 90.20%;
  descent-override: 22.48%;
  line-gap-override: 0%;
}

This CSS can be dropped directly into your stylesheet. The generated fallback font family name follows a convention that makes it easy to identify which custom font it matches.

Next.js Font Optimization

If you're using Next.js with the next/font module, fallback font metric matching is automatic:

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
});

Next.js generates the appropriate fallback @font-face with all override descriptors calculated from the font's actual metrics. The CSS is inlined into the page, so there are zero additional network requests. This is the best-practice approach for Next.js applications.

Capsize

The Capsize library (available as a JavaScript package and a web tool) calculates typographic metrics from a font file and generates CSS properties that produce predictable, layout-stable text rendering:

import { precomputeValues } from '@capsizecss/core';
import inter from '@capsizecss/metrics/inter';
import arial from '@capsizecss/metrics/arial';

const interFallback = precomputeValues({
  fontSize: 16,
  fontMetrics: inter,
  lineGap: 0,
});

// Use the computed values to create a matching fallback

Capsize is more complex than fontaine but provides deeper control over metric matching and works well in design systems that need precise typographic control.

Browser DevTools Experimental Feature

Chrome DevTools has an experimental "Font Editor" panel that shows a font's ascent, descent, and line gap values visually. While it doesn't generate override CSS automatically, it provides a quick visual reference for understanding how two fonts' metrics compare.


Putting It All Together

A complete CLS-prevention setup for a site using Inter from Google Fonts:

/* 1. Load the custom font */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

/* 2. Create a metric-matched fallback */
@font-face {
  font-family: 'Inter-Fallback';
  src: local('Arial');
  size-adjust: 107.04%;
  ascent-override: 90.20%;
  descent-override: 22.48%;
  line-gap-override: 0%;
}

/* 3. Use both in the font stack */
body {
  font-family: 'Inter', 'Inter-Fallback', Arial, sans-serif;
}

And in the HTML:

<!-- Preload Inter to minimize the window where fallback shows -->
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>

This combination — metric-matched fallback, preloaded font, and font-display: swap — produces font swaps that are nearly invisible to the human eye. The text may technically shift by a pixel or two during the swap, but the CLS score drops to near zero, and users experience no perceptible layout jump.

For zero-CLS font loading without this complexity, font-display: optional combined with preloading prevents swaps entirely at the cost of potentially showing the fallback font on first visit. Both approaches are valid; the metric-matching approach is better when brand consistency on first visit matters.


Certain page designs are particularly susceptible to font-induced CLS. Understanding the common patterns helps you anticipate and fix problems before they reach production.

Hero Section Text Reflow

The most impactful single CLS source is typically the above-the-fold hero section. If your hero uses a large heading in a custom display font, and the fallback font is substantially wider or taller, the entire page layout can shift when the custom font loads.

Fix: Preload the display heading font and use font-display: optional to prevent swapping, or use size-adjust to minimize the reflow when the swap occurs.

@font-face {
  font-family: 'Playfair Display Fallback';
  src: local('Georgia');
  size-adjust: 115%;
  ascent-override: 86%;
  descent-override: 21%;
  line-gap-override: 0%;
}

.hero-heading {
  font-family: 'Playfair Display', 'Playfair Display Fallback', Georgia, serif;
}

Navigation bars with inline text often change height when fonts load, pushing the entire page content down. A nav bar that's 48px tall with Arial might expand to 52px with a font that has a larger line height, causing all content below the nav to shift by 4px — small but enough to trigger a CLS violation.

Fix: Set an explicit height or min-height on your navigation element that accounts for the custom font's metrics, preventing any size change during font loading.

.nav {
  height: 64px;  /* Fixed height prevents font-induced CLS */
  display: flex;
  align-items: center;
}

Inline Elements Causing Reflow

Inline elements within paragraphs — links, inline code, emphasized text — can sometimes cause unexpected CLS when they use the same font as surrounding text but have a different computed size due to font metrics differences.

Monitor for small, surprising CLS events in DevTools using the Layout Shift Regions overlay. These small shifts are often caused by inline elements you wouldn't immediately suspect.


Testing Font CLS in CI

Manual DevTools inspection is useful during development, but automating CLS testing in your CI pipeline ensures regressions are caught before they reach production.

Playwright + Web Vitals

// tests/font-cls.spec.js
const { test, expect } = require('@playwright/test');
const { getCLS } = require('web-vitals');

test('font loading CLS is below threshold', async ({ page }) => {
  let clsScore = 0;

  await page.addInitScript(() => {
    // Collect CLS from web-vitals
    window.clsValues = [];
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          window.clsValues.push(entry.value);
        }
      }
    }).observe({ type: 'layout-shift', buffered: true });
  });

  await page.goto('https://yoursite.com');

  // Wait for fonts to load
  await page.waitForFunction(() => document.fonts.ready);
  await page.waitForTimeout(2000);  // Wait for any pending swaps

  clsScore = await page.evaluate(() =>
    window.clsValues.reduce((sum, v) => sum + v, 0)
  );

  expect(clsScore).toBeLessThan(0.1);
});

This test runs the page in a real browser, waits for fonts to load and swap, then measures the accumulated CLS. A failing test (CLS > 0.1) catches font metric changes, new font additions, or subsetting changes that impact layout stability.

Lighthouse CI

Lighthouse CI can enforce CLS budgets as part of your CI/CD pipeline:

{
  "ci": {
    "assert": {
      "assertions": {
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
      }
    }
  }
}

Run this on every pull request to prevent font-related CLS regressions from shipping to production.

Typography Terms

Try These Tools

Fonts Mentioned

Related Articles