浏览器页面渲染流程
渲染流程
浏览器渲染流程是什么?
渲染的过程其实就是将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);
};
}
最后
以上就是结实大山最近收集整理的关于浏览器页面渲染流程浏览器页面渲染流程的全部内容,更多相关浏览器页面渲染流程浏览器页面渲染流程内容请搜索靠谱客的其他文章。
发表评论 取消回复