Web字体优化实战:从加载阻塞到性能提升的完整指南

By | 2026年7月11日

引言

你是否遇到过这样的情况:页面加载时,文字先是显示为系统字体,然后突然闪烁变成设计字体?这就是著名的“FOUT”(Flash of Unstyled Text)或“FOIT”(Flash of Invisible Text)。Web字体虽然让页面更美观,但若不加优化,会严重影响用户体验和性能指标。本文将带你深入理解Web字体的加载过程,并提供一套完整的优化方案,包括字体子集化、预加载、font-display策略、缓存优化等,每一步都有可运行的代码示例。

1. 理解字体加载的“罪魁祸首”

当浏览器遇到CSS中的@font-face时,会下载字体文件。在下载完成之前,浏览器有两种行为:

  • FOIT:隐藏文字(最多3秒),等待字体加载。
  • FOUT:立即使用后备字体,字体加载后替换。

默认情况下,大多数浏览器采用FOIT,这会导致文字不可见,严重影响首次内容绘制(FCP)和最大内容绘制(LCP)。

2. 第一步:使用font-display控制字体渲染策略

font-display是CSS属性,用于控制字体的加载行为。它有五个值:autoblockswapfallbackoptional

  • swap:立即使用后备字体,加载完成后替换(推荐用于品牌字体)。
  • optional:给字体极短时间(约100ms)加载,若超时则使用后备字体,且不替换(适合图标字体)。
  • fallback:类似optional,但替换窗口更短。

代码示例


@font-face {
    font-family: 'MyCustomFont';
    src: url('/fonts/myfont.woff2') format('woff2');
    font-display: swap; /* 避免FOIT */
    font-weight: 400;
    font-style: normal;
}

body {
    font-family: 'MyCustomFont', 'Helvetica', sans-serif;
}

注意swap可能导致布局偏移(CLS),因为后备字体和自定义字体的度量不同。可以通过调整size-adjust或使用font-size-adjust来缓解。

3. 第二步:预加载关键字体

使用可以提前下载字体,避免CSS解析后才开始下载。

代码示例


<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>

注意:必须添加crossorigin属性,因为字体请求通常是跨域请求(即使同域)。

最佳实践:只预加载首屏需要的字体,避免过度预加载。

4. 第三步:字体子集化——只加载需要的字符

字体文件通常包含数千个字符,但你的页面可能只用到其中一小部分(例如拉丁字母)。通过子集化,可以大幅减小字体文件大小。

工具:使用glyphhangerFontTools

使用glyphhanger示例


# 安装
global install glyphhanger

# 分析页面中使用的字符
glyphhanger https://example.com --spider

# 生成子集字体(只包含用到的字符)
glyphhanger --whitelist="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" --formats=woff2 --output=./subset-fonts

代码集成


@font-face {
    font-family: 'MySubsetFont';
    src: url('/fonts/myfont-subset.woff2') format('woff2');
    unicode-range: U+0020-007E; /* 只覆盖ASCII字符 */
    font-display: swap;
}

注意unicode-range告诉浏览器何时使用该字体,避免下载不需要的字体。

5. 第四步:使用WOFF2格式并压缩

WOFF2是当前最佳字体格式,压缩率比WOFF高30%。确保服务器支持WOFF2。

转换工具:使用google/woff2或在线工具。


# 使用woff2_compress
woff2_compress myfont.ttf

服务器配置:添加正确的MIME类型。


# Nginx
location /fonts/ {
    add_header Content-Type font/woff2;
}

6. 第五步:利用缓存策略

字体文件通常不常变,应设置强缓存。

HTTP头示例


Cache-Control: public, max-age=31536000, immutable

Nginx配置


location ~* \.(woff2|woff)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

7. 第六步:使用字体加载API进行精细控制

CSS Font Loading API允许用JavaScript监听字体加载状态,实现更精细的交互。

代码示例


const font = new FontFace('MyCustomFont', 'url(/fonts/myfont.woff2)', {
    display: 'swap'
});

font.load().then(() => {
    document.fonts.add(font);
    console.log('Font loaded');
    // 可以在此添加类,避免布局偏移
    document.body.classList.add('font-loaded');
}).catch(err => {
    console.error('Font loading failed', err);
});

8. 完整优化流程示例

假设你有一个自定义字体Poppins,我们将其优化。

步骤1:子集化

使用glyphhanger提取页面用到的字符,生成子集字体。

步骤2:CSS配置


@font-face {
    font-family: 'Poppins';
    src: url('/fonts/poppins-subset.woff2') format('woff2');
    font-display: swap;
    font-weight: 400;
    font-style: normal;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

body {
    font-family: 'Poppins', 'Helvetica Neue', sans-serif;
}

/* 后备字体大小调整,减少CLS */
body {
    font-size: 16px;
    line-height: 1.5;
}

步骤3:HTML预加载


<link rel="preload" href="/fonts/poppins-subset.woff2" as="font" type="font/woff2" crossorigin>

步骤4:服务器配置

Nginx配置缓存和MIME类型。

9. 常见坑与踩坑经验

  • 坑1:预加载未加crossorigin导致字体被加载两次。
  • 坑2font-display: swap导致布局偏移,未使用size-adjustascent-override等度量调整。
  • 坑3:子集化时遗漏了某些字符(如标点符号),导致页面显示空白或乱码。
  • 坑4:同时加载多种字体变体(如粗体、斜体),每个都需要单独@font-face。

10. 总结

通过以上步骤,你可以显著提升Web字体的加载性能:

  • 使用font-display: swap避免FOIT
  • 预加载关键字体
  • 子集化减少文件大小
  • 使用WOFF2格式
  • 设置强缓存
  • 利用JavaScript API精细控制

延伸阅读