字体加载太慢怎么优化?

シ芳芳 阅读 13

我在项目里用了一个自定义的中文字体,但页面首次加载时明显卡顿,文字会先显示默认字体再闪成自定义字体,用户体验很差。我试过用 font-display: swap,但好像没完全解决问题。

现在想用 JavaScript 监听字体加载完成后再显示内容,但不确定写法对不对,下面这段代码在部分浏览器里根本不触发回调:

const font = new FontFace('MyCustomFont', 'url(./fonts/custom.woff2)');
font.load().then(() => {
  document.fonts.add(font);
  document.body.classList.add('font-loaded');
}).catch(err => {
  console.warn('字体加载失败', err);
});
我来解答 赞 2 收藏
二维码
手机扫码查看
1 条解答
世昌(打工版)
这个问题我太熟了,之前做项目的时候被中文字体坑过好几次。中文字体文件动不动几MB,加载慢是必然的。你说的闪烁问题,专业术语叫FOUT(Flash of Unstyled Text),咱们一步步来解决。

先说你这段代码的问题。FontFace API本身没写错,但有几个坑:

第一个坑是兼容性。document.fonts 这个API在Safari上支持得比较晚,iOS 10以前根本不支持。你说的"部分浏览器不触发回调",大概率就是兼容性问题。

第二个坑是路径问题。url(./fonts/custom.woff2) 这个相对路径,在某些构建工具打包后可能会出问题,建议用绝对路径或者让构建工具处理。

咱们分几步来彻底解决这个问题:

第一步,最简单有效的方案是用CSS preload预加载字体。在HTML的head里加上:



这样做的好处是浏览器会提前下载字体,不用等到CSS解析到font-face才去下载。注意crossorigin属性必须加,不然预加载会失效,这个坑我踩过。

第二步,你的font-display: swap其实用对了,但它就是会闪烁,这是它的设计初衷——先显示文字,字体加载完再替换。如果你不想闪烁,可以改用font-display: block,但这样文字会延迟显示,用户体验也不一定好。

更推荐的做法是用font-display: optional。这个属性很聪明,如果字体在很短时间内(通常是100ms)加载完了就用自定义字体,没加载完就用系统字体,而且不会出现闪烁。当然缺点就是用户可能看不到自定义字体,但至少体验流畅。

第三步,如果你坚持要用JS监听字体加载,我来给你一个更稳妥的写法:

// 先检测浏览器是否支持 FontFace API
if ('fonts' in document) {
const font = new FontFace('MyCustomFont', 'url(/fonts/custom.woff2)');

font.load().then(function(loadedFont) {
// 字体加载成功,添加到document.fonts
document.fonts.add(loadedFont);

// 方案A:添加class让CSS控制字体显示
document.body.classList.add('font-loaded');

// 方案B:直接应用字体到某个元素
// document.body.style.fontFamily = '"MyCustomFont", sans-serif';
}).catch(function(error) {
console.error('字体加载失败:', error);
// 加载失败时的降级处理
});

// 额外监听所有字体加载完成的事件
document.fonts.ready.then(function() {
console.log('所有字体加载完成');
});
} else {
// 降级方案:不支持FontFace API的浏览器直接显示
document.body.classList.add('font-loaded');
}


这里有个关键点:document.fonts.ready 会在所有CSS字体都加载完成后触发,比单独监听某个字体更靠谱。

第四步,配合CSS来做初始隐藏。页面一开始先把内容隐藏,字体加载完再显示:

/* 初始状态隐藏文字 */
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}

.content {
opacity: 0;
transition: opacity 0.3s ease;
}

/* 字体加载完成后显示 */
body.font-loaded .content {
opacity: 1;
font-family: 'MyCustomFont', sans-serif;
}


但是!这里有个问题,如果字体加载失败或者JS报错,用户就看不到内容了。所以一定要加个超时处理:

// 设置超时,不管字体有没有加载完,超过3秒就显示内容
const timeoutId = setTimeout(function() {
document.body.classList.add('font-loaded');
}, 3000);

// 字体加载完成后清除超时
document.fonts.ready.then(function() {
clearTimeout(timeoutId);
document.body.classList.add('font-loaded');
});


第五步,也是最关键的一步,针对中文字体的优化:字体子集化。

中文字体几MB甚至十几MB,全部加载肯定慢。你只需要把页面实际用到的字提取出来,生成一个子集字体。用font-spider或者fontmin这类工具:

npm install fontmin -g

fontmin -t "你要显示的文字内容" -o ./dist ./fonts/custom.ttf


或者在构建流程里用font-spider,它会自动扫描HTML里用到的所有汉字,生成精简版字体。我之前把一个8MB的中文字体子集化后只有30KB,加载速度直接起飞。

第六步,考虑用woff2格式。你已经在用了,这点很好。woff2比woff压缩率更高,现代浏览器都支持。但记得要保留woff作为降级方案:

@font-face {
font-family: 'MyCustomFont';
src: url('/fonts/custom.woff2') format('woff2'),
url('/fonts/custom.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: optional;
}


总结一下,推荐的完整方案是:preload预加载 + 字体子集化 + font-display: optional + JS超时降级。这套组合拳下来,基本能解决你的问题。

对了,还有个坑要注意:如果你用Webpack或者Vite,记得配置字体文件的hash命名和缓存策略,不然用户每次访问都要重新下载字体。配置好强缓存后,第二次访问就会从本地读取,体验会好很多。
点赞
2026-02-28 16:04