# 前端浏览器渲染原理
# 1. 浏览器渲染过程
浏览器渲染页面的整个过程可以分为以下几个步骤:
- 解析HTML生成DOM树
- 解析CSS生成CSSOM树
- 将DOM树和CSSOM树合并生成渲染树
- 布局计算
- 绘制到屏幕
# 2. DOM树构建
浏览器会将HTML文档解析成一个由DOM元素组成的树形结构,即DOM树。解析过程中,遇到script标签会阻塞DOM树的构建,直到脚本执行完毕。

- 转换: 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。
- 令牌化: 将字符串转换成Token,例如:
<html>、<body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息 - 词法分析: 发出的令牌转换成定义其属性和规则的“对象”
- DOM构建: 由于 HTML 标记定义不同标记之间的关系(一些标记包含在其他标记内),创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,body 是 paragraph 对象的父项,依此类推。
# 3. CSSOM树构建
与处理 HTML 时一样,我们需要将收到的 CSS 规则转换成某种浏览器能够理解和处理的东西。因此,我们会重复 HTML 过程,不过是为 CSS 而不是 HTML:

CSS 字节转换成字符,接着转换成令牌和节点,最后链接到一个称为“CSS 对象模型”(CSSOM) 的树结构内

浏览器解析css过程是阻塞的,浏览器需要解析完所有的css才会使用css样式(和浏览器的回流重绘一样)
CSS它只显示了我们决定在样式表中替换的样式。每个浏览器都提供一组默认样式(也称为User Agent 样式),即我们不提供任何自定义样式时所看到的样式,我们的样式只是替换这些默认样式(例如默认 IE 样式 (opens new window))
# 3.1 CSS 的层叠性
层叠性 该层叠将获取给定元素上给定属性的声明值的无序列表,按声明的优先级对它们进行排序,并输出单个层叠值。
当多个相互冲突的CSS声明应用于同一个元素时,CSS层叠算法会根据一定的机制,从最高权重到最低权重的顺序列出著作权归作者所有,具体如下:
来源和重要性 -> 选择器权重 -> 出现的顺序 -> 初始和继承属性(默认值)
# 3.2 CSS的继承性
继承性 当元素的一个继承属性没有指定值时,则取父元素的同属性的计算值。只有文档根元素取该属性的概述中给定的初始值
color、 text-开头的、line-开头的、font-开头的样式可以被继承
# 4. 渲染树构建
浏览器将 DOM 和 CSSOM 合并成一个“渲染树”,网罗网页上所有可见的 DOM 内容,以及每个节点的所有 CSSOM 样式信息

为构建渲染树,浏览器大体上完成了下列工作:
从 DOM 树的根节点开始遍历每个可见节点
- 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
- 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,上例中的 span 节点不会出现在渲染树中,因为有一个显式规则在该节点上设置了“display: none”属性
对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们
发射可见节点,连同其内容和计算的样式
请注意 visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分
# 5. 渲染树布局
布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小
# 5.1 分层
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树
拥有层叠上下文属性的元素会被提升为单独的一层

从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性
需要剪裁(clip)的地方也会被创建为图层 需要剪裁也就是说容器内容不足以显示页面内容,出现了滚动条,渲染引擎会为这部分单独创建一个层
# 6. 渲染树绘制
在绘制阶段,遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制
# 6.1 栅格化(raster)
要明白栅格化,先要理解什么是图块和位图。
图块(tile) 图块是渲染进程即浏览器内核当中的合成线程将图层划分为大小512x512或者256x256的区块


位图 位图是栅格化的过程:合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的,将图块转换为位图。
# 6.2 合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上
# 7. 渲染流水线总结
从HTML到DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面一张图是总结下这整个渲染流程

- 渲染进程将HTML内容转换为能够读懂的
DOM树结构。 - 渲染引擎将CSS样式表转化为浏览器可以理解的
styleSheets,计算出DOM节点的样式。 - 创建
布局树,并计算元素的布局信息。 - 对布局树进行分层,并生成
分层树。 - 为每个图层生成
绘制列表,并将其提交到合成线程。 - 合成线程将图层分成
图块,并在光栅化线程池中将图块转换成位图。 - 合成线程发送绘制图块命令
DrawQuad给浏览器进程。 - 浏览器进程根据DrawQuad消息生成页面,并
显示到显示器上。
# 8. 回流与重绘
# 8.1 回流/重排(Reflow)
意味着元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout

从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
# 8.2 常见引起回流/重排属性的方法
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等)
- 元素字体大小变化
- 添加或删除可见的DOM元素
- 激活CSS伪类(如:hover)
具体内容如下
- width/height
- margin/padding/border
- display/position/overflow
- clientWidth/clientHeight
- clientTop/clientLeft
- offsetWidth/offsetHeight
- offsetTop/offsetLeft
- scrollWidth/scrollHeight
- scrollTop/scrollLeft
- scrollIntoView()
- scrollTo()
- getComputedStyle()
- getBoundingClientRect()
- scrollIntoViewIfNeeded()
# 8.3 重绘/重绘(Repaint)
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘
从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
常见引起重绘属性的方法
- visibility
- color
- border-style/border-radius
- background/background-image/background-position/background-repeat/background-size
- text-decoration
- outline-color/outline/outline-style/outline-width
- box-shadow
# 8.4 回流与重绘的关系
回流必定会引起重绘,而重绘不一定会引起回流。
# 8.5 浏览器的渲染队列
当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作
强制刷新队列 因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘
强制刷新队列的style样式请求:
offsetTop、offsetLeft、offsetWidth、offsetHeight、 scrollTop、scrollLeft、scrollWidth、scrollHeight clientTop、clientLeft、clientWidth、clientHeight getComputedStyle(), 或者 IE的 currentStyle
我们在开发中,应该谨慎的使用这些style请求,注意上下文关系,避免一行代码一个重排,这对性能是个巨大的消耗
# 8.6 优化策略
# 8.6.1 分离读写操作
// bad 4次重排+重绘
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
// good 一次重排
div.style.left = '10px';
div.style.top = '10px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
2
3
4
5
6
7
8
9
10
# 8.6.2 样式集中改变
// bad
const left = 10;
const top = 10;
el.style.left = `${left }px`;
el.style.top = `${top }px`;
// good
el.className += 'className';
// good
el.style.cssText += `; left: ${ left }px; top: ${ top }px;`;
2
3
4
5
6
7
8
9
# 8.6.3 离线改变dom
- 隐藏要操作的dom
在要操作dom之前,通过display隐藏dom,当操作完成之后,才将元素的display属性为可见,因为不可见的元素不会触发重排和重绘
- 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素
const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
2
3
4
# 8.6.4 避免触发同步布局事件
当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,强制刷新队列,所以避免使用这些属性
# 8.6.5 对于复杂动画效果,使用绝对定位让其脱离文档流
对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流
# 8.6.6 使用transform替代position
主线程需要做的任务如下:
- 运行Javascript
- 计算HTML元素的CSS样式
- layout (relayout)
- 将页面元素绘制成一张或多张位图
- 将位图发送给合成线程
合成线程主要任务是:
- 利用GPU将位图绘制到屏幕上
- 让主线程将可见的或即将可见的位图发给自己
- 计算哪部分页面是可见的
- 计算哪部分页面是即将可见的(当你的滚动页面的时候)
- 在你滚动时移动部分页面
当用户滚动一个页面时,合成线程会让主线程提供最新的可见部分的页面位图。然而主线程不能及时的响应。这时合成线程不会等待,它会绘制已有的页面位图。对于没有的部分则绘制白屏。
- transform 属性不会影响元素的布局或样式,只是改变元素的视觉呈现,例如位置、大小、旋转角度等。由于它操作的是 合成层(compositing layer) ,完全由 GPU 负责计算和渲染,因此不会触发重排或重绘。
- transform 会使用 GPU 硬件加速,性能更好;position + top/left 会触发大量的重绘和回流,性能影响较大
- 硬件加速的工作原理是创建一个新的复合图层,然后使用合成线程进行渲染
- 使用GPU可以优化动画效果,但是不要滥用,会有内存问题
- 理解强制触发硬件加速的 transform 技巧,使用对GPU友好的CSS属性
- 使用 transform 和 opacity,实现高性能动画,减少重排和重绘。借助 GPU 渲染的合成层优化动画效果。
# 9. 渲染性能监控
# 9.1 使用Performance API
// 监控长任务
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('长任务:', entry);
}
}
});
observer.observe({ entryTypes: ['longtask'] });
// 监控首屏渲染时间
function measureFirstPaint() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime}`);
}
});
observer.observe({ entryTypes: ['paint'] });
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 9.2 使用Lighthouse进行性能分析
// 在开发环境中使用Lighthouse API
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
async function runLighthouse(url) {
const chrome = await chromeLauncher.launch({chromeFlags: ['--headless']});
const options = {logLevel: 'info', output: 'html', onlyCategories: ['performance']};
const runnerResult = await lighthouse(url, options);
// 输出性能报告
console.log('性能评分:', runnerResult.lhr.categories.performance.score);
await chrome.kill();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 参考文章
渲染树构建、布局及绘制 (opens new window)
浏览器的回流与重绘 (Reflow & Repaint) (opens new window)
你真的了解回流和重绘吗 (opens new window)
前端性能优化之浏览器渲染优化 (opens new window)
Google Web Fundamentals - Rendering Performance (opens new window)