概述
浏览器页面渲染流程
渲染流程
浏览器渲染流程是什么?
渲染的过程其实就是将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 插入 console.time('DocumentFragment插入耗时') const list = document.getElementById('ul'); let insertCount = 30000; let count = 0; function fragmentRender() { const fragment = document.createDocumentFragment(); while (count <= insertCount) { const li = document.createElement('li'); li.innerHTML = `DocumentFragment记录${count}`; fragment.appendChild(li); count++; } if (count > insertCount) { list.appendChild(fragment); } } fragmentRender(); console.timeEnd('DocumentFragment插入耗时'); // DocumentFragment插入耗时: 151.240234375 ms
-
通过
requestAnimationFrame
(rAF
)的方式循环插入DOM
现代浏览器提供了
requestAnimationFrame``API
来解决非常耗时的代码段对渲染的阻塞问题.,DocumentFragment
可以减少DOM
操作次数,requestAnimationFrame
可以保证新节点操作在页面重绘前执行,二者结合可以实现数据渲染优化.// DocumentFragment && requestAnimationFrame console.time('rAF插入耗时'); const list = document.getElementById('ul'); const total = 30000; // 需要插入的节点数 const insertCount = 51; // 一次插入的节点数 const insertTimes = Math.ceil(total / insertCount); // 需要处理的批次数 let finishCount = 0; // 已经插入的批次数 function fragmentRender() { const fragment = document.createDocumentFragment(); const count = total - (finishCount * insertCount); // 未插入的节点数 const forCount = count >= insertCount ? insertCount : count; // 需要遍历的节点数 for (let i = 0; i < forCount; i++) { const li = document.createElement('li'); li.innerHTML = `rAF记录${finishCount * insertCount + i + 1}`; fragment.appendChild(li); } list.appendChild(fragment); finishCount++; rAfRender(); } function rAfRender() { if (finishCount < insertTimes) { window.requestAnimationFrame(fragmentRender); } } rAfRender(); console.timeEnd('rAF插入耗时'); // rAF插入耗时: 0.15771484375 ms
-
虚拟滚动
virtualized scroller
,这种技术的原理就是只渲染可视区域内的内容,非可见区域的就完全不渲染.当用户滚动的时候,实时替换渲染的内容.# 浏览器页面渲染流程
渲染流程
浏览器渲染流程是什么?
渲染的过程其实就是将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 插入 console.time('DocumentFragment插入耗时') const list = document.getElementById('ul'); let insertCount = 30000; let count = 0; function fragmentRender() { const fragment = document.createDocumentFragment(); while (count <= insertCount) { const li = document.createElement('li'); li.innerHTML = `DocumentFragment记录${count}`; fragment.appendChild(li); count++; } if (count > insertCount) { list.appendChild(fragment); } } fragmentRender(); console.timeEnd('DocumentFragment插入耗时'); // DocumentFragment插入耗时: 151.240234375 ms
-
通过
requestAnimationFrame
(rAF
)的方式循环插入DOM
现代浏览器提供了
requestAnimationFrame``API
来解决非常耗时的代码段对渲染的阻塞问题.,DocumentFragment
可以减少DOM
操作次数,requestAnimationFrame
可以保证新节点操作在页面重绘前执行,二者结合可以实现数据渲染优化.// DocumentFragment && requestAnimationFrame console.time('rAF插入耗时'); const list = document.getElementById('ul'); const total = 30000; // 需要插入的节点数 const insertCount = 51; // 一次插入的节点数 const insertTimes = Math.ceil(total / insertCount); // 需要处理的批次数 let finishCount = 0; // 已经插入的批次数 function fragmentRender() { const fragment = document.createDocumentFragment(); const count = total - (finishCount * insertCount); // 未插入的节点数 const forCount = count >= insertCount ? insertCount : count; // 需要遍历的节点数 for (let i = 0; i < forCount; i++) { const li = document.createElement('li'); li.innerHTML = `rAF记录${finishCount * insertCount + i + 1}`; fragment.appendChild(li); } list.appendChild(fragment); finishCount++; rAfRender(); } function rAfRender() { if (finishCount < insertTimes) { window.requestAnimationFrame(fragmentRender); } } rAfRender(); console.timeEnd('rAF插入耗时'); // rAF插入耗时: 0.15771484375 ms
-
虚拟滚动
virtualized scroller
,这种技术的原理就是只渲染可视区域内的内容,非可见区域的就完全不渲染.当用户滚动的时候,实时替换渲染的内容.
requestAnimationFrame
在web
应用中,实现动画效果的方法比较多:
Javascript
可以通过定时器setTimeout
实现,CSS3
可以通过transition
和animation
实现,html5
中的canvas
也可以实现.除此之外,html5
还提供一个专门用于请求动画的API
,即requestAnimationFrame
(rAF),顾名思义,就是"请求动画帧".
requestAnimationFrame
解决了什么问题?与setTimeout
有什么区别?
requestAnimationFrame
与setTimeout
类似,与之相比最大的优势是,rAF
是由系统来决定回调函数的执行时机
.具体一点讲就是,系统每次绘制之前会主动调用 rAF 中的回调函数
,如果系统绘制率是 60Hz
,那么回调函数就每16.7ms
被执行一次,如果绘制频率是75Hz
,那么这个间隔时间就变成了 1000/75=13.3ms
。换句话说就是,rAF
的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次
,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
requestAnimationFrame
解决了setTimeout
定时器时间间隔不稳定的问题,setTimeout
中的回调是异步执行的,需要等待同步任务以及微任务执行完毕后再执行,因此在定时器中设定的时间(如16.67ms
)是不准确的,这就会导致动画延迟,效果不精确等问题.
requestAnimationFrame
怎么使用?兼容性怎么处理?
requestAnimationFrame
的使用方法与setTimeout
基本相同,注意clearTimeout
对应的是cancelAnimationFrame
.
也正因如此,可以使用setTimeout
作为处理requestAnimationFrame
兼容性的备用方法:
if (!window.requestAnimationFrame) {
requestAnimationFrame = function(fn) {
setTimeout(fn, 16.67);
};
}
最后
以上就是结实大山为你收集整理的浏览器页面渲染流程浏览器页面渲染流程的全部内容,希望文章能够帮你解决浏览器页面渲染流程浏览器页面渲染流程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复