浏览器渲染原理
HTML 文件转换为 DOM 树
打开网页的时候,浏览器会请求对应的 HTML 文件。其实就是由字符串组成的一个文件,但是计算机硬件是不理解这些字符串的,所以在网络传输的内容其实是 0 和 1 这些字节数据。当浏览器介绍到这些字节数据后,它将这些字节数据转换成字符串,也就是我们写的代码。
字节数据 => 字符串(代码)
当数据转换成字符串后,浏览器会将这些字符串通过词法分析转换为标记(token),这一过程在词法分析中叫标记化(tokennization)
字节数据 => 字符串 => Token
标记还是字符串,是构成代码的最小单位。这一过程将代码分拆为一块块,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思。
当结束标记化后,这些标记会紧接着转换为 Node,最后这些 Node 会根据不同 Node 之前的联系构建为一颗 DOM 树
整个转换过程
字节数据 => 字符串 => Token => Node => DOM
CSS 文件转换为 CSSOM 树
转换 CSS 到 CSSOM 树的过程和 HTML 类似
字节数据 => 字符串 => Token => Node => CSSOM
这一过程,浏览器确认每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式可以自行设置给某个节点,也可以通过继承获得。这一个过程,浏览器递归 CSSOM 树,然后确定元素到底是什么样式。
为什么会消耗资源?
<div>
<a> <span></span> </a>
</div>
<style>
span {
color: red;
}
div > a > span {
color: red;
}
</style>
对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span
标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span
标签,然后找到 span
标签上的 a
标签,最后再去找到 div
标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS
选择器,然后对于 HTML
来说也尽量少的添加无意义标签,保证层级扁平。
生产渲染树
DOM 树 + CSSOM 树 => 渲染树。
这一过程,不是简单的两者合并。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none
的,那么就不会在渲染树中显示。
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。
整个步骤:
加载整体
HTML
文件至上而下解析
HTML
解析
HTML
建立DOM
树,遇到诸如<script>
、<link>
等标签时,就会去下载相应内容,并解析、执行。如果是
<link>
标签,解析CSS
构建CSSOM
树DOM
和CSSOM
结合生成render
树布局
render
树(Layout/reflow),负责各元素尺寸、位置的计算绘制
render
树(paint),绘制页面像素信息浏览器会将各层的信息发送给
GPU
,GPU
会将各层合成(composite),显示在屏幕上。
重排和重绘
当浏览器打开任何一个页面,在页面首次渲染时,后面会伴随着一系列操作,如 JavaScript 脚本动态操作 DOM 或 CSSOM,用户的输入,异步加载,动效,用户滚动页面,用户调整浏览器视窗大小等,都会在首次渲染的基础上进行更新。
所以,页面首次都会进行一次绘制。在这以后,对构建渲染树信息进行任何改变都会造成两种结果:
渲染树的部分(全部)内容需要重新验证,并重新计算节点尺寸(布局有变化)。这个过程就是重排或者回流、布局
页面部分内容需要获取更新,如节点(Node)的几何信息变化,或者一些类似背景颜色之类的 CSS 样式上的变化(样式有变化)。这个过程就是 重绘
页面的初始布局至少会重排一次;重绘不一定导致重排,但重排一定会导致重绘。
重排(reflow):DOM 布局发生改变,引起重排(改变窗口大小,字体大小...)
重绘(repaint):元素的 CSS 属性发生改变,引起重绘(color,border-style,background...)
引起重排的操作有:
- 页面首次渲染。
- 浏览器窗口大小发生改变。
- 元素尺寸或位置发生改变。
- 元素内容变化(文字数量或图片大小等等)。
- 元素字体大小变化。
- 添加或者删除可见的
DOM
元素。 - 激活
CSS
伪类(例如::hover)。 - 设置
style
属性 - 查询某些属性或调用某些方法。
常见引起重排属性和方法
width
, height
, border
...
减少重绘和回流
- 使用
transform
替代top
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>
使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局)修改频繁的元素先
display: none
,修改完之后显示不要逐条修改
DOM
样式,尽量提前设置好class
,后续增加class
,进行批量修改使用
documentFragment
对象在内存里操作DOM
不要把节点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
不要使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
CSS
选择符从右往左匹配查找,避免节点层级过多将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于
video
标签来说,浏览器会自动将该节点变为图层。
设置节点为图层的方式有很多,我们可以通过以下几个常用属性可以生成新图层
- will-change
- video、iframe 标签
操作 DOM 慢
操作 DOM
性能很差,但是其中是什么原因?
因为 DOM
是属于渲染引擎中的东西,而 JS
又是 JS
引擎中的事物,当我们通过 JS
操作 DOM
的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来性能上的损耗。操作 DOM
次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM
可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
插入几万个 DOM,如何实现页面不卡顿?
分批次部分渲染
DOM
requestAnimationFrame
虚拟滚动(virtualized scroller):只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容
什么情况阻塞渲染
渲染的前提是生成渲染树;所以 HTML 和 CSS 肯定会阻塞渲染。
渲染越快,应降低渲染文件的大小,并且避免多层无意义嵌套,优化选址器。
当浏览器在解析到 script
标签的时,会暂停构建 DOM
,浏览器必须等脚本下载完,并执行结束,如果想首屏渲染越快,就越不应该在首屏加载 JS
文件,这也是建议将 script
标签放在 body
标签底部的原因。
在现代,并不必须将 script
标签放在底部,因为可以给标签添加 defer
或 async
属性
defer
表示 JS
文件会并行下载,但是会放到 HTML
解析完成后顺序执行
告诉浏览器不要等待 JS
脚本。相反,浏览器将继续处理 HTML
,构建 DOM
。脚本会“在后台”下载,然后等 DOM
构建完成后,脚本才会执行
如果多个 JS
文件都存在 defer
属性时:
<script defer src="a.js"></script>
<script defer src="b.js"></script>
它们会并行下载;但是,defer
除了告诉浏览器“不要阻塞页面”之外,还会保持 JS
执行的相对顺序,因此,即使 b.js
先加载完成,它人需要等到 a.js
执行结束才会被执行。
提示
defer
特性仅适用于外部脚本
如果 <script>
脚本没有 src
,则会忽略 defer
特性。
async
表示 JS
文件下载不会阻塞渲染(解析应该是会阻塞渲染的)
JS
是完全独立的
浏览器不会因
async
脚本而阻塞其他
JS
不会等待async
脚本加载完成,同样,async
脚本也不会等待其他JS
同时具有
async
属性的JS
,谁先加载谁执行,不保证顺序加载
当我们将独立的第三方脚本集成到页面时,此时采用异步加载方式是非常棒的:计数器,广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们
提示
async
特性仅适用于外部脚本
如果 <script>
脚本没有 src
,则会忽略 async
特性。
在不考虑缓存和优化网络协议的前提下,考虑可以通过哪些方式来最快的渲染页面
当发生 DOMContentLoaded
事件后,就会生成渲染树,生成渲染树就可以进行渲染了,这一过程更大程度上和硬件有关系了。
如何加速:
文件大小
script
标签使用HTML
、CSS
代码书写格式首屏是否需要的文件
一些建议:
减少文件大小,尽量压缩
script
标签异步加载CSS 不要写多层级选择器,HTML 减少无意义标签,扁平化,避免多层无意义嵌套
首页内容尽量优化,减少不必要加载或延后加载
对首页布局样式无影响的 JS 延后加载