第2章:浏览器渲染原理——关键渲染路径(CRP)
聊到前端性能,绕不开一个核心概念——关键渲染路径(Critical Rendering Path,简称CRP)。
说白了,就是浏览器从拿到HTML代码,到最终在屏幕上画出像素的整个过程。我当年刚入行时,觉得这玩意儿太底层,跟业务开发没啥关系。直到有一次,一个简单的列表页加载慢得像蜗牛,我翻来覆去找原因,最后发现是CSS阻塞了渲染。嗯,从那以后,我再也不敢小看CRP了。
2.1 关键渲染路径的五个步骤
浏览器渲染一个页面,大致走五步:
- 构建DOM树——解析HTML,生成节点树
- 构建CSSOM树——解析CSS,生成样式树
- 合并成渲染树——DOM + CSSOM = Render Tree
- 布局(Layout)——计算每个节点的几何位置
- 绘制(Paint)——把像素画到屏幕上
你想想看,这五步是串行的。哪一步卡住了,页面就出不来。我见过不少团队,只顾着压缩图片、优化接口,却忽略了CSS和JS的加载顺序——结果首屏还是慢。
2.2 DOM树构建——从字节到节点
浏览器拿到HTML后,先做词法分析,把字节流转换成字符,再解析成标签,最后构建成节点树。
这个过程叫「解析」。但有个坑——HTML解析是边下载边解析的,不是等全部下载完再解析。所以,如果HTML文件很大,浏览器会分段处理。
我在项目中遇到过一个问题:一个页面有3000多行HTML,全是服务端渲染的表格行。结果首屏渲染时间长达8秒。后来发现,浏览器解析这3000行时,DOM树太大,光构建就花了2秒多。
2.3 CSSOM树构建——渲染的拦路虎
CSSOM的构建和DOM类似,但有一个关键区别:CSS是阻塞渲染的。
为什么?因为浏览器不知道CSS规则什么时候加载完。如果CSS没加载完就渲染,页面可能会出现「无样式内容闪烁」(FOUC)。所以,浏览器干脆等CSSOM构建完,再开始渲染。
我习惯把CSS分为三类:
| 类型 | 加载方式 | 是否阻塞渲染 |
|---|---|---|
| 关键CSS | 内联在<head>中 | 是 |
| 非关键CSS | 异步加载(media="print" 或 rel="preload") | 否 |
| 第三方CSS | 延迟加载(defer) | 否 |
举个例子,你写了一个按钮样式,放在外部CSS文件里。如果这个CSS文件加载慢,整个页面都得等着。我建议把首屏用到的样式内联到HTML里,剩下的异步加载。
.nav .item a 这个选择器,浏览器会先找所有 a 标签,再往上找 .item,最后找 .nav。所以,选择器越具体,匹配越慢。我建议尽量用类选择器,少用后代选择器。
2.4 渲染树——DOM和CSSOM的合体
DOM树和CSSOM树构建完成后,浏览器把它们合并成渲染树(Render Tree)。
渲染树只包含可见节点。像 <head>、<script>、display: none 的元素,都不会出现在渲染树里。
这里有个容易混淆的点:visibility: hidden 的元素会出现在渲染树里,因为它占位置;而 display: none 不会。我刚开始做前端时,以为两者一样,结果布局老是出问题。
display: none 隐藏的「幽灵节点」。
2.5 布局(Layout)——算位置、算大小
布局阶段,浏览器计算每个可见节点的几何信息:宽、高、x、y坐标。
这个过程也叫「回流」(Reflow)。回流很消耗性能,因为一旦某个元素的位置或尺寸变了,浏览器得重新计算它和它子元素、父元素、兄弟元素的位置。
我遇到过最夸张的一次:一个页面里用了大量绝对定位,而且每个绝对定位元素都依赖父容器计算位置。结果每次滚动,浏览器都要重新布局,页面卡得不行。
哪些操作会触发回流?
- 改变窗口大小(resize)
- 修改元素的宽高、边距、边框
- 添加或删除可见DOM元素
- 改变字体大小
- 读取某些属性(如
offsetHeight、scrollTop)
offsetHeight,再批量修改样式。这样浏览器可以合并回流,而不是读一次改一次,来回折腾。
2.6 绘制(Paint)——画像素
布局完成后,浏览器开始绘制。绘制就是把每个节点的视觉样式(颜色、背景、阴影、边框)画成像素。
绘制阶段也分两步:
- 生成绘制指令——浏览器把每个元素的绘制命令记录下来
- 栅格化——把指令转换成屏幕上的实际像素
绘制本身不慢,但如果有大量复杂的视觉效果(比如多个阴影、渐变、圆角),绘制时间会明显增加。
我记得有一次优化一个卡片列表,每个卡片都有 box-shadow 和 border-radius。结果列表滚动时,帧率掉到20fps。后来我把阴影改成用伪元素模拟,绘制时间直接降了一半。
transform 和 opacity 做动画,因为它们不会触发重绘,而是走合成线程。
2.7 总结:CRP优化的三个方向
说了这么多,其实CRP优化就三个方向:
- 减少关键资源数量——内联关键CSS,异步加载非关键CSS和JS
- 减少关键路径长度——压缩HTML、CSS、JS,减少请求次数
- 减少关键字节数——用更小的文件,更少的代码
我个人习惯在项目初期就做一次CRP分析。打开Chrome DevTools的Performance面板,录一次加载过程,看看DOMContentLoaded和First Paint的时间。如果这两个时间差距大,多半是CSS或JS阻塞了渲染。
好了,这一章就到这里。下一章我们聊聊「资源加载策略:预加载、预连接与预渲染」,到时候我会分享一个真实项目的优化案例——那个页面从5秒降到1.2秒,就是靠这些技巧。
公众号:蓝海资料掘金营,微信deep3321