浏览器页面渲染流程
渲染流程
浏览器渲染流程是什么?
渲染的过程其实就是将url
对应的各种资源, 通过浏览器渲染引擎的解析,输出可视化的图像:
HTML/CSS/JavaScript => 浏览器渲染引擎 => 图像
-
浏览器解析
HTML
文件为DOM
树当我们打开一个网页,浏览器请求对应的
HTML
(在网络传输中是0和1的字节数据),将这些字节数据转换为字符串(我们写的代码).接着再将字符串通过词法分析转换为标记(
token
),这一过程在词法分析中称为标记化(tokenization
).那么什么是标记呢?这其实属于编译原理这一块的内容了。简单来说,标记还是字符串,是构成代码的最小单位。这一过程会将代码分拆成一块块,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思
结束标记后,这些标记会紧接着被转换为
Node
,最后根据这些Node
之间的联系,构建DOM
树.字节数据 => 字符串 => token => Node => DOM树
-
将
CSS
文件转换为CSSOM
树(CSS
对象模型树)这一过程与构建
DOM
树是相似的.字节数据 => 字符串 => token => Node => CSSOM树
在这一个过程中,浏览器会确认每一个节点的样式是什么,并且这个过程是很消耗资源的(样式的设置是多样化的).因此,我们应该尽可能的避免写过于具体的
CSS
选择器,如div > a > span
,然后对于HTML
来说,也尽量少的添加无意义标签,保证层级扁平.根据页面渲染流程可得知:
css
加载不会阻塞DOM
树的解析,但会阻塞DOM
树的渲染;css
加载会阻塞后面js
语句的执行
-
生成
Render Tree
(渲染树)生成
DOM
树和CSSOM
树后,就会将这两颗树组合为渲染树.这一过程并不是简单的合并,`render tree`只会包括需要显示的节点和这些节点的样式信息,比如,如果某个节点的样式是`display:none`,就不会在`render tree`中显示.
-
浏览器生成
render tree
后,就会根据render tree
来进行布局(也叫回流或者重排),然后调用GPU
绘制,合成图层,显示在屏幕上.
阻塞渲染
什么情况会阻塞渲染?怎么解决?
阻塞 | 解决 |
---|---|
渲染的前提首先是生成渲染树,因此HTML 和CSS 的解析肯定会阻塞渲染 |
应该从一开始降低需要渲染的文件大小,比如HTML 保证层级扁平,CSS 优化选择器 |
浏览器解析到script 标签时,会暂停DOM 的构建.解析完js 后才会从暂停的地方重新开始构建 |
不应该在首屏加载js 文件,将script 标签至于body 底部(当然,也可以添加defer 和async 属性)defer 属性表示该js 文件会并行下载,但是会放到HTML 解析完成后执行.对于没有任何依赖的 js 文件可以添加async 属性,表示js 文件的下载和解析不会阻塞渲染. |
重绘&回流
- 重绘(
repaint
): 当渲染树中的元素外观(如:color
)发生改变,不影响布局时,产生重绘; - 回流(
reflow
): 当渲染树中的元素布局(如:尺寸,位置,隐藏状态)发生改变时,产生回流(重排); - 当
JS
获取Layout
的属性值(如:offsetLeft,scrollTop,getComputedStyle
等),也会引起回流,因为浏览器需要通过回流重新计算最新的值; - 回流必将引起重绘,而重绘不一定会引起回流;
如何针对重绘和回流进行前端优化?
- 需要对元素进行复杂的操作时,可以先隐藏 该元素(
display:none
),操作完成以后,再显示; - 需要创建多个
DOM
节点时,使用DocumentFragment
创建完后一次性地加入document
; - 缓存
Layout
的属性值,如:let left = elem.offsetLeft
,这样多次使用left
只产生第一次的回流; - 尽量避免使用
table
布局,table
元素一旦触发回流就会导致table
里所有的其他元素回流; - 尽量避免
css
表达式(expression
),因为每次调用都会重新计算值(包括加载页面); - 尽量使用
css
属性的简写,如用border
代替border-width,border-style,border-color
; JS
中批量修改元素的样式,如:elem.className
和elem.style.cssText
代替elem.style.xxx
;
DOM
操作
操作
DOM
性能为什么会变差?
DOM
属于渲染引擎,JS
属于JS
引擎,通过JS
操作DOM
涉及了两个线程之间的通信,势必会带来一些性能的损耗.操作DOM
的次数一多,就等同于一直在进行线程之间的通信.- 操作
DOM
可能会带来重绘和回流的情况.
经典面试题:插入几万个
DOM
,怎么实现页面不卡顿?
首先,不可能把几万个DOM
一次性插入,这样做是绝对会卡顿的,解决问题的关键应该从减少DOM操作次数
和缩短循环时间
两个方面去减少主线程阻塞的时间.
-
DocumentFragment
减少
DOM
操作次数的良方是createDocumentFragment API
,它用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点.DocumentFragment
节点不属于文档树,继承的parentNode
属性总是null
.DocumentFragment
有一个很实用的特点,当请求把一个DocumentFragment
节点插入文档树时,插入的不是DocumentFragment
自身,而是它的所有子孙节点.这使得它起到了一个暂存节点的作用.因此,当需要添加多个dom
元素时,如果先将这些元素添加到DocumentFragment
中,再统一将DocumentFragment
添加到DOM
树种的节点,可以减少页面渲染DOM
的次数,效率明显提升.以下是原生插入3万个节点和利用
DocumentFragment
插入3万个节点的对比:// 原生插入 console.time('原生插入耗时') const list = document.getElementById('ul'); let insertCount = 30000; // 插入的节点数 let count = 0; // 当前已插入的节点数 function protoRender() { while (count <= insertCount) { const li = document.createElement('li'); li.innerHTML = `原生插入节点${ count}`; list.appendChild(li); count++; } } protoRender(); console.timeEnd('原生插入耗时'); // 原生插入耗时: 154.080322265625 ms
// DocumentFragment 插入 consol