渲染与性能
从 HTML 到像素:浏览器渲染流水线
弄清 Style、Layout、Paint、Composite 各自做什么,就能判断一次样式修改要付出多少代价,也知道动画该用哪些属性。
五个阶段
浏览器把代码变成屏幕上的像素,要经过一条流水线:
- Parse(解析):把 HTML 解析成 DOM,把 CSS 解析成 CSSOM。
- Style(样式计算):算出每个元素最终生效的样式。
- Layout(布局):算出每个元素的位置和大小。
- Paint(绘制):把文字、颜色、边框、阴影等绘制成图层的内容。
- Composite(合成):把多个图层按正确的顺序合成到屏幕上。
关键在于:修改样式之后,流水线会从受影响的阶段开始,把后面的步骤都走一遍。越靠前的阶段被触发,代价越大。
改什么,就要付什么代价
| 修改的属性 | 会触发 | 代价 |
|---|---|---|
width、height、top、margin、font-size | Layout → Paint → Composite | 高,可能波及周围元素 |
color、background、box-shadow | Paint → Composite | 中 |
transform、opacity | 通常只有 Composite | 低,可以在合成线程上完成 |
所以做动画时应当优先使用 transform 和 opacity。把元素从左移到右,用 transform: translateX() 比改 left 流畅得多:前者通常不需要重新布局,即使主线程正在忙,动画也能继续运行。
/* 每一帧都会触发布局 */
.box-slow { transition: left 0.3s; }
/* 通常只需要合成 */
.box-fast { transition: transform 0.3s; }will-change: transform 可以提前把元素提升为独立图层,但每个图层都要占用内存。只应该在确实有动画的元素上使用,而不是全局铺开。
强制同步布局:看不见的性能杀手
通常浏览器会把多次样式修改攒到一起,在下一帧统一处理。但如果你改完样式后马上读取 offsetWidth、getBoundingClientRect() 这类布局信息,浏览器为了给出准确的值,只能立刻做一次布局。在循环里反复这样做,就是所谓的“布局抖动”:
// 差:每次循环都强制一次布局
for (const box of boxes) {
box.style.width = box.offsetWidth + 1 + 'px';
}
// 好:先统一读,再统一写
const widths = boxes.map((box) => box.offsetWidth);
boxes.forEach((box, i) => {
box.style.width = widths[i] + 1 + 'px';
});下面的示例会在你的浏览器里对 1000 个元素真正运行这两种写法,显示实际耗时:
这个互动示例需要启用 JavaScript。
脚本和样式怎样阻塞页面
解析 HTML 时遇到普通的 <script>,解析器必须停下来等脚本下载并执行完,因为脚本可能修改文档。这就是脚本标签要加 defer 或 async 的原因:
| 写法 | 下载 | 执行时机 | 顺序 |
|---|---|---|---|
<script> | 阻塞解析 | 下载完立刻执行 | 按文档顺序 |
<script defer> | 并行 | HTML 解析完之后、DOMContentLoaded 之前 | 按文档顺序 |
<script async> | 并行 | 下载完立刻执行,此时会暂停解析 | 谁先下载完谁先执行 |
<script type="module"> | 并行 | 默认和 defer 相同 | 按文档顺序 |
一般来说,依赖 DOM 或者彼此有依赖关系的脚本用 defer;统计这类独立脚本用 async。CSS 则会阻塞渲染:在首屏需要的样式加载完之前,浏览器不会绘制页面,以免用户看到没有样式的内容闪一下。
让屏幕外的内容晚点渲染
长页面可以用 content-visibility: auto 跳过屏幕外区块的布局和绘制工作。配合 contain-intrinsic-size 给出一个估计高度,可以避免滚动条跳动:
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}用 DevTools 验证
猜测性能问题很容易猜错,直接看工具更可靠:
- Performance 面板:录制一段操作,紫色是 Style 和 Layout,绿色是 Paint。红色三角通常意味着长任务或强制布局。
- Rendering 面板:打开 Paint flashing,可以看到哪些区域在重绘。