4、字体渲染事件:字体渲染的时机、重绘与回流对字体渲染的影响、字体渲染的优化策略
字体渲染这事儿,看着简单,其实坑不少。很多同学觉得「字体嘛,设个 font-family 就完事了」。但实际项目中,字体加载的时机、浏览器何时重绘、何时回流,都会直接影响页面的呈现效果和性能。我这些年踩过的坑,大多跟「字体闪了一下」或者「布局突然跳了一下」有关。
说白了,字体渲染不是一个瞬间动作,而是一连串事件的组合。你得知道浏览器在什么时候、因为什么原因去重新渲染字体,才能精准地控制它。
4.1 字体渲染的时机:浏览器什么时候开始画字?
浏览器解析 HTML 和 CSS 时,遇到 @font-face 或 font-family 声明,并不会立刻下载字体文件。它有一个「延迟加载」的策略。我个人习惯把这个过程拆成三个阶段:
- 样式计算阶段:浏览器构建 CSSOM,发现字体引用,标记为「待加载」。
- 字体下载阶段:浏览器开始异步下载字体文件,此时页面可能先用后备字体占位。
- 字体交换阶段:字体下载完成后,浏览器触发重绘,用新字体替换旧字体。
关键点:字体下载是异步的,不会阻塞 DOM 解析,但会阻塞「字体渲染」本身。也就是说,文字会先显示出来,等字体到了再换——这就是「FOUT」(Flash of Unstyled Text)或「FOIT」(Flash of Invisible Text)的根源。
我在项目中遇到过一个问题:页面首屏加载时,标题字体用了很重的 woff2 文件,结果 3 秒后才显示出来,中间一片空白。用户还以为页面坏了。这就是典型的 FOIT 问题。
4.2 重绘与回流:字体渲染的「蝴蝶效应」
先区分两个概念:
- 回流(Reflow):元素的尺寸、位置发生变化,浏览器需要重新计算布局。
- 重绘(Repaint):元素的视觉样式发生变化,但不影响布局,浏览器只需重新绘制。
字体渲染跟这两者都有关系。你想想看,字体变了,文字宽度可能变,行高可能变,甚至整个段落的换行位置都会变。这就触发了回流。回流之后,浏览器必然重绘。
| 触发场景 | 影响范围 | 性能开销 |
|---|---|---|
| 字体文件加载完成,替换后备字体 | 所有使用该字体的元素 | 高(可能触发大面积回流) |
| 字体大小变化(如响应式调整) | 该元素及其子元素 | 中 |
| 字体粗细、样式变化 | 该元素 | 低(通常只重绘) |
| 字体渲染引擎切换(如 GPU 光栅化) | 整个页面 | 高 |
注意:回流是性能杀手。一次字体替换可能引发数百个 DOM 节点的重新布局。我曾经在一个数据表格页面中,因为字体加载导致整列宽度重新计算,页面卡了将近 1 秒。
4.3 字体渲染的优化策略:让字体「静悄悄」地出现
优化字体渲染,核心目标就一个:减少视觉突变,降低性能开销。我总结了四条实战策略:
4.3.1 使用 font-display 控制加载行为
font-display 是 CSS 提供的属性,告诉浏览器在字体加载期间怎么处理文字。我最常用的是 swap 和 optional:
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* 先用后备字体显示,字体到了再换 */
}
/* 或者 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: optional; /* 字体在 100ms 内没加载完,就用后备字体,不再替换 */
}
嗯,这里要注意:swap 虽然解决了白屏问题,但会带来布局抖动。如果你对页面布局稳定性要求高,optional 更合适——它只在极短时间内等待字体,超时就直接放弃。
4.3.2 预加载关键字体
对于首屏必须使用的字体,可以用 <link rel="preload"> 提前下载:
<link rel="preload" href="/fonts/heading.woff2" as="font" type="font/woff2" crossorigin>
这样浏览器在解析 CSS 之前就开始下载字体,能显著缩短 FOIT/FOUT 的时间窗口。我习惯把首屏标题字体和正文主体字体都做预加载。
4.3.3 字体子集化与格式选择
字体文件越大,下载越慢,渲染越晚。优化字体文件本身是根本:
- 子集化:只保留页面用到的字符。比如中文页面只保留常用汉字,可以去掉生僻字和拉丁字符。文件体积能减少 60%-80%。
- 格式选择:优先使用 woff2,它比 woff 小 30% 左右,比 ttf 小一半以上。
小技巧:我用过 glyphhanger 和 fonttools 做子集化。对于中文站点,建议保留 3500-5000 个常用字,基本覆盖 99% 的日常阅读场景。
4.3.4 避免字体引发的回流
这是最容易被忽略的一点。字体替换导致回流,本质上是「新旧字体度量不同」造成的。解决方案有两个:
- 使用度量相近的后备字体:选择跟目标字体宽高比接近的字体作为 fallback。比如用
system-ui或Arial替代无衬线字体。 - 固定容器尺寸:给文字容器设置固定宽高,或者使用
min-height,即使字体变了,布局也不会跳动。
.heading {
font-family: 'MyFont', 'Arial', sans-serif;
font-size: 2rem;
/* 固定高度,避免回流 */
min-height: 2.4em;
line-height: 1.2;
}
我曾经在一个电商首页用这个方法,把字体加载导致的布局抖动从 300ms 降到了几乎不可感知。用户根本不知道字体换过。
4.4 知识体系总览
下面这张图把字体渲染的时机、影响和优化策略串起来了。你可以把它当作本章的「思维导图」:
看完这张图,你应该能理解字体渲染不是一个孤立事件,而是跟浏览器的布局引擎、绘制管线深度绑定的。控制好时机,选对策略,字体就能「静悄悄」地出现在用户眼前。