跳到主要内容

字体优化

问题

前端如何优化 Web 字体加载?什么是 FOIT 和 FOUT?如何进行字体子集化?

答案

Web 字体是影响页面加载性能和用户体验的关键因素。字体文件通常较大(100KB-1MB),如果处理不当会导致文本闪烁、布局偏移甚至内容不可见。


字体加载问题

FOIT 和 FOUT

现象全称描述影响
FOITFlash of Invisible Text字体加载时文本不可见用户无法阅读内容
FOUTFlash of Unstyled Text先显示后备字体,后切换视觉闪烁
FOFTFlash of Faux Text先显示合成样式,后替换布局偏移

性能影响


font-display 属性

font-display 控制字体加载时的渲染行为。

@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* 关键属性 */
}
阻塞期交换期行为
auto浏览器决定浏览器决定默认行为
block3s无限FOIT,最多等待 3s
swap0无限FOUT,立即显示后备字体
fallback100ms3s短暂不可见后尝试加载
optional100ms0可能不使用 Web 字体

推荐策略

/* 正文字体:优先内容可见 */
@font-face {
font-family: 'BodyFont';
src: url('/fonts/body.woff2') format('woff2');
font-display: swap;
}

/* 图标字体:短暂不可见可接受 */
@font-face {
font-family: 'IconFont';
src: url('/fonts/icons.woff2') format('woff2');
font-display: block;
}

/* 装饰字体:可选择性使用 */
@font-face {
font-family: 'FancyFont';
src: url('/fonts/fancy.woff2') format('woff2');
font-display: optional;
}

字体格式选择

格式扩展名压缩兼容性推荐度
WOFF2.woff2Brotli现代浏览器⭐⭐⭐
WOFF.woffgzipIE9+⭐⭐
TTF.ttf全部
EOT.eotIE
/* 现代写法:只需 WOFF2 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
}

/* 兼容写法:渐进增强 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2'),
url('/fonts/custom.woff') format('woff'),
url('/fonts/custom.ttf') format('truetype');
}

字体子集化

字体子集化是只保留需要的字符,大幅减小文件体积。

中文字体优化

中文字体通常包含 2-3 万个字符,完整字体可达 10MB+。

# 使用 fonttools 子集化
pip install fonttools brotli

# 提取常用 3500 字
pyftsubset source.ttf \
--text-file=chars.txt \
--output-file=subset.woff2 \
--flavor=woff2

# 按 Unicode 范围
pyftsubset source.ttf \
--unicodes="U+4E00-U+9FFF" \
--output-file=chinese.woff2 \
--flavor=woff2

动态子集化

// 使用 Google Fonts 的 text 参数
const text = encodeURIComponent('欢迎访问我的网站');
const fontUrl = `https://fonts.googleapis.com/css2?family=Noto+Sans+SC&text=${text}`;

unicode-range 分片

/* 按 Unicode 范围分片加载 */
@font-face {
font-family: 'NotoSansSC';
src: url('/fonts/noto-basic.woff2') format('woff2');
unicode-range: U+0000-00FF; /* 基本拉丁 */
}

@font-face {
font-family: 'NotoSansSC';
src: url('/fonts/noto-chinese-1.woff2') format('woff2');
unicode-range: U+4E00-4FFF; /* 中文片段 1 */
}

@font-face {
font-family: 'NotoSansSC';
src: url('/fonts/noto-chinese-2.woff2') format('woff2');
unicode-range: U+5000-51FF; /* 中文片段 2 */
}
/* 浏览器只下载页面实际使用的字符集 */

字体预加载

<!-- 预加载关键字体 -->
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
>

<!-- 预连接字体服务器 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

JavaScript 预加载

// 使用 FontFace API
async function preloadFont(name: string, url: string) {
const font = new FontFace(name, `url(${url})`);

try {
const loadedFont = await font.load();
document.fonts.add(loadedFont);
console.log(`Font ${name} loaded`);
} catch (error) {
console.error(`Failed to load font ${name}:`, error);
}
}

preloadFont('CustomFont', '/fonts/custom.woff2');

// 检测字体是否已加载
document.fonts.ready.then(() => {
console.log('All fonts loaded');
document.body.classList.add('fonts-loaded');
});

// 检测特定字体
document.fonts.check('16px CustomFont'); // true/false

后备字体优化

使用相似的后备字体减少 FOUT 导致的布局偏移。

字体匹配

/* 使用 size-adjust 调整后备字体大小 */
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 105%; /* 调整到与 Web 字体相似 */
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
}

body {
font-family: 'CustomFont', 'Fallback', sans-serif;
}

使用 @font-face 覆盖

/* 定义优化的系统字体栈 */
@font-face {
font-family: 'SystemStack';
src: local('San Francisco'),
local('-apple-system'),
local('BlinkMacSystemFont'),
local('Segoe UI'),
local('Roboto'),
local('Helvetica Neue'),
local('Arial');
}

字体加载策略

关键字体优先

// FOFT 策略:Flash of Faux Text
// 1. 先加载子集(只含常用字符)
// 2. 再加载完整字体

async function loadFontsWithFOFT() {
// 加载子集字体
const subsetFont = new FontFace(
'MainFont',
'url(/fonts/main-subset.woff2)',
{ weight: '400' }
);

await subsetFont.load();
document.fonts.add(subsetFont);
document.body.classList.add('subset-loaded');

// 异步加载完整字体
const fullFont = new FontFace(
'MainFont',
'url(/fonts/main-full.woff2)',
{ weight: '400' }
);

fullFont.load().then(font => {
document.fonts.add(font);
document.body.classList.add('full-loaded');
});
}

条件加载

// 只在需要时加载字体
function loadFontOnDemand(text: string) {
// 检查是否包含特殊字符
const hasSpecialChars = /[\u4e00-\u9fff]/.test(text);

if (hasSpecialChars && !document.fonts.check('16px "Chinese"')) {
return preloadFont('Chinese', '/fonts/chinese.woff2');
}
}

// 监听用户交互
document.addEventListener('focus', (e) => {
if (e.target instanceof HTMLInputElement) {
loadFontOnDemand(e.target.placeholder);
}
}, true);

性能监控

// 监控字体加载时间
const fontLoadStart = performance.now();

document.fonts.ready.then(() => {
const fontLoadTime = performance.now() - fontLoadStart;
console.log(`Fonts loaded in ${fontLoadTime}ms`);

// 上报性能数据
sendAnalytics('font_load_time', fontLoadTime);
});

// 监控 CLS
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.sources?.some(s => s.node?.tagName === 'P')) {
console.log('Text CLS:', entry.value);
}
}
}).observe({ type: 'layout-shift', buffered: true });

最佳实践清单


常见面试问题

Q1: 什么是 FOIT 和 FOUT?如何解决?

答案

问题描述解决方案
FOIT字体加载时文本不可见font-display: swap
FOUT后备字体闪烁为 Web 字体优化后备字体匹配
/* 解决 FOIT */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* 立即显示后备字体 */
}

/* 减少 FOUT 闪烁 */
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 105%; /* 调整后备字体大小 */
}

Q2: font-display 各值的区别?

答案

阻塞期行为适用场景
block3s不可见等待图标字体
swap0立即后备正文(推荐)
fallback100ms短暂不可见平衡体验
optional100ms可能不用装饰字体

Q3: 如何优化中文字体?

答案

  1. 字体子集化:只包含使用的字符
  2. unicode-range 分片:按需加载字符集
  3. 使用 CDN:Google Fonts 支持动态子集
  4. 本地字体优先local() 检测系统字体
/* 按需加载中文字符 */
@font-face {
font-family: 'Chinese';
src: url('/fonts/chinese-common.woff2') format('woff2');
unicode-range: U+4E00-U+9FFF;
}

Q4: 字体预加载的注意事项?

答案

<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin <!-- 必须添加,即使同源 -->
>

注意事项:

  1. 必须添加 crossorigin:字体加载默认匿名
  2. 只预加载关键字体:首屏使用的字体
  3. 使用正确的 type:type="font/woff2"
  4. 避免过度预加载:会占用带宽

Q5: 如何检测字体是否加载完成?

答案

// 1. CSS 类切换
document.fonts.ready.then(() => {
document.body.classList.add('fonts-loaded');
});

// 2. 检测特定字体
const isFontLoaded = document.fonts.check('16px "CustomFont"');

// 3. FontFace API
const font = new FontFace('CustomFont', 'url(/fonts/custom.woff2)');
font.load().then(loadedFont => {
document.fonts.add(loadedFont);
});

// 4. 监听字体加载状态
document.fonts.onloadingdone = (e) => {
console.log('Fonts loaded:', e.fontfaces);
};

相关链接