浏览器到底是怎么把 HTML 渲染到屏幕上的?

HTML1周前发布 admin
117 0

写了这么多年前端代码,我们每天都在跟 HTMLCSSJS 打交道,但很少有人真正停下来想一想:从我们敲下 <div> 到用户在屏幕上看到一个个像素点,中间到底发生了什么?

这篇文章尝试把浏览器渲染这条”流水线”完整地捋一遍,希望能帮你在做性能优化时,知道自己到底在优化哪一步。

一、渲染的起点:拿到字节流

浏览器渲染的第一步,是网络进程把服务器返回的 HTML 数据(本质上是一连串字节)交给渲染进程。渲染进程需要先把这些字节,按照声明的编码(比如 UTF-8)解码成字符,再把字符解析成一个个 Token(词法分析阶段)。

这个过程遵循的是 WHATWG 制定的 HTML 解析规范,规范里详细定义了各种”奇葩”写法该怎么容错处理——比如标签没闭合、属性没加引号等等。这也是为什么浏览器对写得很随意的 HTML 也能”抢救”着渲染出来。

二、构建 DOM 树

拿到 Token 之后,浏览器会用一个叫 Tree Construction 的算法,把 Token 组装成一棵树状结构,也就是我们常说的 DOM(Document Object Model)

DOM 树忠实反映了 HTML 标签之间的父子、兄弟关系。需要注意的是,这一步和后面的 CSS、JS 处理并不是完全割裂的——如果 HTML 中间嵌了一个没有 async/defer<script> 标签,HTML 解析器会暂停,等 JS 下载并执行完之后再继续解析后面的内容。这就是为什么”JS 会阻塞 DOM 解析”这句话的由来。

三、构建 CSSOM

和 DOM 类似,浏览器也要把 CSS 解析成一棵树,叫 CSSOM(CSS Object Model)

CSSOM 记录了每个节点最终会应用哪些样式规则,包括浏览器默认样式、外部样式表、内联样式,以及它们之间因为”层叠”(Cascade)规则产生的优先级覆盖关系。这一步的计算量其实不小,选择器越复杂、样式表越大,计算 CSSOM 的开销就越高。这也是为什么业界通常建议 CSS 选择器不要写得过深、过于复杂。

CSSOM 的构建会阻塞渲染——浏览器必须等样式规则完全确定后,才能进行下一步,否则页面可能会出现”先展示无样式内容,再突然套上样式”的闪烁,所以 CSS 通常被称为”渲染阻塞资源”。

四、合并成渲染树(Render Tree)

有了 DOM 和 CSSOM,浏览器接下来会把二者合并,生成 渲染树(Render Tree)

这里有个容易被忽略的细节:渲染树上的节点,和 DOM 节点并不是一一对应的。比如设置了 display: none 的元素,压根不会出现在渲染树里(但设置 visibility: hidden 的元素会出现,只是不可见);<head> 里的内容、注释节点等也不会进入渲染树。渲染树只包含”最终真的要画到屏幕上”的那些节点及其计算后的样式。

五、布局(Layout / Reflow)

有了渲染树,浏览器还不知道每个节点具体该画在屏幕的什么位置、占多大空间。这一步叫 布局(Layout),老一点的资料里也常叫它 Reflow(回流)

布局阶段会从渲染树的根节点开始,递归计算每个节点的几何信息:宽高、边距、在视口中的精确坐标等等。这里有个关键特性——布局是会连锁反应的。比如父容器的宽度变了,子元素大概率也要重新计算;一个元素的高度变化,可能会导致它后面的兄弟元素、乃至整个页面的布局都跟着重排。这就是为什么频繁读写会触发布局的属性(比如 offsetHeightgetBoundingClientRect()),在循环里交替读写会造成”强制同步布局”,是常见的性能陷阱。

六、绘制(Paint)

布局确定了”东西在哪、多大”,但还不知道”每个像素具体是什么颜色”。绘制(Paint) 阶段负责把渲染树转换成实际的绘制指令——填充背景色、画边框、渲染文字、画阴影等等,这些指令会被记录下来,交给下一步处理。

值得一提的是,浏览器不会把整个页面当成一张画布来画,而是会根据一定规则(比如设置了 will-change、3D 变换、video/canvas 元素、有 overflow 滚动的区域等)把页面拆分成多个 图层(Layer),分别绘制。合理利用图层拆分,是很多动画性能优化技巧的底层原理。

七、合成(Composite)

最后一步是 合成(Composite)。浏览器的合成线程会把之前生成的各个图层,按照正确的层叠顺序拼合到一起,交给 GPU 完成最终的屏幕绘制。

这一步之所以重要,是因为它可以脱离主线程独立运行。如果一个动画只涉及 transformopacity 这两个属性的变化,浏览器往往只需要重新合成,而不需要重新走布局和绘制这两个开销较大的阶段。这也是为什么业界普遍推荐”用 transform 做动画,而不是改 left/top“——前者可能只触发合成,后者则可能触发一整条布局+绘制+合成的完整流水线。

八、把这条流水线串起来:关键渲染路径

把以上几步连起来,就是常说的 关键渲染路径(Critical Rendering Path):

HTML 字节流 → 解析 Token → 构建 DOM
CSS 字节流 → 解析 → 构建 CSSOM
DOM + CSSOM → 渲染树
渲染树 → 布局(计算几何信息)
布局结果 → 绘制(生成绘制指令)
绘制结果 → 合成(多图层拼合,GPU 输出到屏幕)

需要强调的是,这不是一条”从头到尾只走一次”的流水线。JS 修改了 DOM、CSS 类名切换、窗口大小变化、字体加载完成等等,都可能让这条流水线的某几个阶段重新执行一遍——具体从哪一步开始重新执行,取决于改动本身影响的范围。这也是为什么理解这条流水线,对写出高性能页面至关重要:知道自己的改动会触发”布局+绘制+合成”全套,还是只触发”合成”,优化的方向就完全不同。

写在最后

浏览器渲染的整个过程,其实就是”数据结构变换”的过程:字节 → Token → DOM/CSSOM → 渲染树 → 几何信息 → 像素指令 → 屏幕图像。每一层转换背后,都藏着规范制定者和浏览器工程师对正确性、容错性和性能的权衡。

理解这条链路,不是为了应付面试题,而是能让我们在写代码时多一份”直觉”——什么样的操作代价大,什么样的操作代价小,从而写出对浏览器更”友好”的页面。

© 版权声明

相关文章

暂无评论

none
暂无评论...