前言
大家好,今天给大家分享一个前端面试中老生常谈的问题:浏览器是如何渲染的?其实渲染过程十分复杂,可能写好几篇长篇大论都讲述不完其中的每个细节,但是本文会以通俗易懂的方式来帮你理清浏览器渲染的基本流程框架,让你对渲染的过程有基本的认识。
输入url后做了些什么
比如,当我们输入www.bilibili.com时:简单来说
-
请求发送到DNS(域名解析服务器),DNS会帮我们将请求发送到bilibili真正的服务器上
-
真正的服务器接收到我们的请求,返回页面数据(
HTML、JS、CSS、图像等) -
我们的浏览器将接收到的资源(HTML、JS、CSS、图像等)进行渲染过程,之后我们就能在浏览器的页面上看到渲染后的bilibili首页了
前两步涉及到计算机网络的知识点,不是我们今天要讲解的知识点,就简略的一笔带过,大家知道这个概念即可,其中第3步的渲染过程就是我们今天讲解的重点,接下来作者会仔细分析这个渲染过程
渲染过程
咱们上文也提到了,实际的渲染过程十分地复杂,但我们的目的是能够弄懂渲染过程的流程,能知道渲染过程基本上做了什么即可。所以本文对照着下面的这张结构图来详细地讲解渲染过程。
这张图乍一看十分的复杂繁琐,没关系,我们慢慢分析!
1️⃣DOM树的下载
要注意:我们接收到服务器传过来的HTML部分,此时还是二进制文件,看我们是如何从二进制文件开始,一步一步将其变成DOM树的吧
1.下载HTML二进制文件:将二进制文件,也就是原始的字节流(里面的内容是010101000...)下载。
2. 转换成字符:将字节流进行字符串化,转换成各个字符,转化通常是按照UTF-8,也可以在编写HTML时指定
3. Tokens化:Token表示词,将字符Token化是一个词法解析的过程,比如:
<div class="text"> ImString</div> //这是我们在“2. 转换成字符”后得到的字符,但是浏览器不懂啊
所以需要Token化:
<div→ 开始标签名class="haha"→ 属性>→ 开始标签闭合haha→ 文本节点</div>→ 结束标签
所以Tokne化就是理解字符串,词法解析的过程
4.转换成节点
这一步就是将我们的内容转换成以节点为单位:
比如
<div class="container">
<h2>欢迎!</h2>
<p>这是一个段落。</p>
<button>点击我</button>
</div>
就里就能转换成以下的节点:
- 整个文档的根节点HTML(包含下面所有的子节点)
<div class="container">节点(包含一个属性节点:class="container")<h2>(包含一个text节点"欢迎!")<p>:(包含一个text节点"这是一个段落。")<button>:(包含一个text节点"点击我")
5.构造DOM树
最后将,所有的节点按照层级关系构建成一颗DOM树,就完成了DOM树的构建,DOM树的根节点是HTML,DOM树的叶子节点通常是标签里面的内容。
至此 DOM树的构建过程就完成了。
整个过程如下图:
2️⃣CSSOM树的下载
CSSOM的树过程很像DOM树的构建,需要注意,CSSOM树的触发通常是在解析DOM树时,遇到了<link>这类标签,从而触发CSS资源下载,然后再像和DOM树 一样 经历以下过程:
CSSOM树的节点是rule节点,由选择器和申明块构成。叶子节点就是具体的申明,如下图
3️⃣JS引擎
你会发现上面的过程没有提到js,是因为js引擎处理的线程和上面提到的渲染过程的线程GUI是不同的
JS的解析其实很不固定,并且js引擎线程和渲染线程GUI会相互阻塞。
比如在构建DOM树时,遇到了一个<script>,则会将控制权由此时的渲染线程GUI交给JS引擎线程,渲染线程GUI此时会进入加入队列等待,只有等到JS引擎线程执行完毕之后,才会继续刚刚的渲染线程GUI
可能有人会好奇为什么要这样做,让JS线程和渲染线程GUI同步进行不好吗,其实这样会涉及一个问题:
可能JS线程此时删除了某个节点, 但是渲染线程GUI还在将这个节点添加到树,以及为这个节点构建CSSOM中的样式节点,这就很没有意义了。
所以我们就需要让JS线程暂时阻塞GUI线程的执行,即阻塞DOM树和CSSOM的构建。
4️⃣合成阶段---变成渲染树
好了,前面的两棵构建完了之后,现在就需要将它们合体(Attachment),构建出一棵渲染树(RenderTree)。
需要注意的一点是:浏览器会并行构建DOM树和CSSOM树:构建DOM树的同时,不影响构建CSSOM树,但是需等待两者都完成后才会生成渲染树。浏览器不会在两者未完成时就提前构建部分渲染树。
整个过程如下:
5️⃣Layout
Layout的过程是什么呢,上面我们提到此时已经构建好了Render Tree树,通过这棵树,我们能够查看页面中有什么元素,以及这些元素的样式和内容,但是此时还有一件事没有做,就是确定每个元素在页面上的具体位置,打个比方说:
<div class="container">
<h2>欢迎!</h2>
<p>这是一个段落。</p>
<button>点击我</button>
</div>
结合对应的css可以看到,这里面的内容的各式各样的样式与内容
但是它在页面上到底应该放在哪个坐标点呢,这就是Layout的作用
在Layout 阶段,浏览器会计算:
<div>的宽度是 800px,高度由子元素撑开<h2>位于(x: 10px, y: 20px),宽度 780px<p>位于(x: 10px, y: 50px),宽度 780px<button>位于(x: 10px, y: 100px),尺寸 100px × 40px
如果没有 Layout,所有元素可能会堆叠在 (0, 0) 位置,导致页面混乱!
6️⃣绘制
有了上一步经过Layout和Render Tree相互合作得到的经过计算的Render Tree之后。
绘制(Paint)阶段会将渲染树转换为具体的页面上的像素,画出位图,这个过程有个专业化的名词叫栅格化。
你以为到这就算结束了,可以看到页面效果了吗,但最后我们还有一步:合成。
7️⃣合成
合成阶段(Composite Layer) 是继绘制(Paint) 之后的最后一步,它属于Painting和Display之间的操作,它同样十分的重要。
它的核心目标是将多个分层的绘制结果高效地合并(Composite)成最终的屏幕图像。这一阶段直接影响到页面的渲染性能和视觉表现(如动画流畅度)
这里讲讲先讲讲分层:
浏览器会将页面分解为多个分层(类似 Photoshop 的图层),每个分层可以独立绘制和合成。以下元素通常会触发分层:
- 显式声明
will-change属性的元素(如will-change: transform)。 - 使用
transform、opacity、filter等 CSS3 属性的元素。 position: fixed或video、canvas等特殊元素。
请注意:使用position: absolute / float 虽然会脱离文档流,但是不会增加新的分层。
合成阶段:
- 分层树(Layer Tree)构建
浏览器根据 CSS 属性(如transform、will-change)生成分层树,决定哪些元素需要独立分层。 - 绘制分层(Paint Layers)
每个分层通过光栅化将绘制指令转换为位图(存储在 GPU 内存中)。 - 合成器(Compositor)处理
合成线程根据分层的位置、层级(z-index)、透明度等属性,计算它们在屏幕上的最终叠加顺序,生成一个合成帧(Compositor Frame) 。 - GPU 渲染
合成帧通过 GPU 快速合并所有分层,输出到屏幕(这一步称为 “光栅操作” 或 “位块传输” )。
在从而我们就能够看到最终的页面效果啦!
至此浏览器的渲染过程就算结束了。
🌟回过头看这张图
让我们再回头看看看这张图,现在我们就能够理解渲染流程了!
重绘与回流
确保理解了上述的渲染流程后,接下来就到了进阶的部分,我们要介绍重绘与回流了
重绘
重绘的概念是指,当页面中的元素的某些样式更改(如颜色、背景色、可见性等变化),而不影响布局,此时浏览器会重新绘制受影响的部分。
结合我们上文的渲染过程:具体表现为假如某个元素仅仅只是元素的颜色发生了改变,此时渲染树中对应会更改被影响的这个元素,但不需要重新计算布局(Layout),然后再一次进行绘制。
回流
回流也叫重排,是指当页面布局或元素几何属性(如尺寸、位置)改变时,浏览器重新计算各元素的位置和大小,更新渲染树并重新布局的过程。
同样结合我们上文的渲染过程:具体表现为假如某个元素的大小发生了改变,影响到了这部分的布局,此时渲染树和布局Layout都会更改,并且需要重新计算,也就是我们之前提到的下图的这个过程,接着根据这个重新计算的的渲染树再一次进行绘制。
请注意:重绘和回流都只会对上面这张图的这个范围内进行动态的修改:而不会影响前面的DOM树和CSSOM树的构建,以及它们的结合成RenderTree这个过程,CSSOM树和DOM树在第一次渲染之后得到一个RenderTree树,我们的重绘和回流必须在这棵树上操作。
如何触发回流
1. 修改几何属性
任何会改变元素尺寸、位置或布局的CSS属性被修改时,都会触发回流:
2. 改变布局结构
DOM结构的改变会导致浏览器重新计算布局:
3. 读取布局属性(强制同步布局)
关键点:在修改样式后立即读取布局属性(如offsetTop、scrollLeft等),会强制浏览器执行同步回流以保证返回最新值:
4. 窗口相关操作
- 窗口大小改变(resize事件)
- 滚动页面(scroll事件)
- 改变视口缩放(viewport zoom)
减少回流
回流一定会触发重绘。但重绘却不一定回流了,它可以单独出现
所以显而易见,回流的开销成本比重绘大,所以我们要尽量减少回流!
下面是几种常见的减少回流的方法:
- 集中修改样式:使用
classList或style.cssText一次性修改样式,而非逐条更改。 - 脱离文档流:对复杂动画元素使用
position: absolute/fixed,减少父元素和其他元素的影响。 - GPU加速:使用
transform和opacity实现分层,分层由GPU渲染,它们不会触发回流 - 避免频繁读写布局属性(如
offsetWidth),可用变量缓存或使用requestAnimationFrame优化。 - 批量DOM操作:使用
DocumentFragment或虚拟DOM(如React/Vue)减少直接操作真实DOM。
实战建议:我们该如何书写代码 优化渲染的性能
理解了浏览器的渲染流程后,我们可以从以下三个关键角度优化代码,提升页面性能:
1. 代码书写顺序
上文提到过
- JS 线程和 GUI 渲染线程互斥
- css的下载需要构造DOM树时触发
所以我们在书写代码时需要调整顺序:浏览器需要 CSSOM 和 DOM 一起构建渲染树(Render Tree),所以尽将CSS放在上方,尽早加载 CSS。
JS放在下方,为了防止频繁阻塞,导致页面频繁卡顿
<head>
<!-- 推荐:head书写css -->
<!-- 提效:早点触发css资源的下载 -->
<link rel = "common.css">
</head>
<body>
...
</body>
<!-- 推荐:body底部书写js -->
<!-- 避免:阻塞DOM构建 -->
<script src="app.js"></script>
2.减少回流(Reflow)
由于回流带来的开销很大,所以我们需要尽可能的减少回流:
- 集中修改样式:使用
classList或style.cssText一次性修改样式,而非逐条更改。 - 脱离文档流:对复杂动画元素使用
position: absolute/fixed,减少父元素和其他元素的影响。 - GPU加速:使用
transform和opacity实现分层,分层由GPU渲染,它们不会触发回流 - 避免频繁读写布局属性(如
offsetWidth),可用变量缓存或使用requestAnimationFrame优化。 - 批量DOM操作:使用
DocumentFragment或虚拟DOM(如React/Vue)减少直接操作真实DOM。
3.减少重绘(Repaint)
同样,尽可能地需要减少重绘
我们可以利用分层,由于有GPU加速,能帮助我们优化
- 优先使用合成属性
/* 触发重绘(但不会回流) */
.element { background: red; }
/* 仅触发合成(最佳性能) */
.optimized {
transform: translateZ(0); /* 启用GPU加速 */
opacity: 0.9;
}
//推荐属性:`transform`、`opacity`、`filter`
//避免滥用 `will-change`(内存消耗增大)
- Canvas/动画优化
// 使用 requestAnimationFrame 替代 setTimeout
function animate() {
element.style.transform = `translateX(${pos}px)`;
pos++;
requestAnimationFrame(animate);
}
animate();
结尾
本篇文章将渲染的整个过程捋了一遍,相信你看完后,此时脑子里能够记住那张图,那个流程了!但是流程中其中每个过程还有非常多非常多的细节和实现,靠一篇文章是讲不完的,如果你感兴趣的话,可以去阅读相关的一些书籍以及文档!
本文部分内容参考isboyjc的「一道面试题」输入URL到渲染全面梳理中-页面渲染篇
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。