浏览器的底层渲染机制
【DOM树】
【CSSOM树】
【Render-Tree渲染树】
总结步骤:
处理 HTML 标记,构建 DOM 树
处理 CSS 标记,构建 CSSOM 树
将 DOM 树和 CSSOM 树融合成渲染树(Render-Tree渲染树)
根据生成的渲染树,计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流 => 布局(Layout)或 重排(reflow)
根据渲染树以及回流得到的几何信息,得到节点的绝对像素 => 绘制(painting)
渲染HTML/CSS代码:遵循W3C规则,GUI(图形用户界面)渲染线程去处理
渲染JS代码:遵循ECMAScript(ECMA-262)规则,JS引擎线程去处理
在渲染代码过程中,如果遇到link/img/script[src=xxx]/audio/video 等,浏览器需要开辟“HTTP线程”,从服务器端获取到对应的资源文件(文件中的代码)
1. GUI在渲染HTML绘制出DOM树(DOM-Tree)
1.1渲染过程中遇到link
则开辟新的HTTP线程去获取资源文件,GUI不受影响,继续向下渲染 “异步”
1.2 如果遇到的是style
, GUI直接去渲染即可“同步”
1.3如果遇到的是@import,也会开辟新的HTTP线程去获取资源,但是此时会阻碍GUI的渲染,只有当样式资源获取后,GUI才会渲染新拿到的样式代码 “同步” ♥♥♥♥♥
==>优化技巧:如果CSS样式“代码比较少”,我们直接使用内嵌式即可;但是如果代码比较多,还用内嵌式则会导致HTML请求速度都很慢,此时我们使用外链式;除特殊必要,不建议使用导入式;
1.4如果遇到img/audio/video 等,和link一样,都是单独分配HTTP线程去获取资源,不会阻碍GUI的渲染!但是真实项目中,第一次加载页面的时候,图片和音视频我们都会做懒加载: ♥♥♥♥♥
同源下允许最多的HTTP并发数是5~7个,所以如果把这些线程用来做图片资源的获取,其他资源都要排后获取了...而且图片本身获取就慢、如果获取的还多,很有可能导致HTTP传输通道的堵塞,让其余正在获取的资源也获取慢了...
虽然图片资源的获取是不会阻碍GUI线程渲染,但是资源回来后,在最后页面渲染的时候,肯定是需要把图片渲染的,这样也延长了页面渲染的时间...
1.5如果遇到的是<script src='xxx.js'>
,它和上面的都不一样,它是“同步的”,它会阻碍GUI的渲染「遇到script,首先分配一个HTTP去获取JS资源,但是同时GUI也暂停渲染了;当资源获取回来后,JS引擎线程开始去渲染JS,此时GUI还等待着呢!」
所以一般都把JS放在页面的底部「不想让其阻碍GUI渲染DOM树」
1.虽然放在底部,不影响DOM树渲染,但是会影响CSSOM树和RENDER树的渲染「毕竟它是阻碍GUI处理的」♥♥♥♥♥
解决方案:把其改为异步操作,不要让他阻碍GUI渲染
<script src="xxx.js" async>
获取资源的时候GUI继续渲染,但是资源一但获取到,立即阻断GUI,继续渲染JS「哪个资源先回来,就把哪个资源先执行,没有考虑JS的依赖顺序」
<script src="xxx.js" defer>
获取资源的时候不会阻碍GUI渲染,但是需要等待GUI渲染完(RENDER-TREE完成),再去按照JS导入的先后顺序,依次渲染JS代码「defer是考虑到了JS依赖顺序的」
优化内容:真实项目中,我们最好都把JS放在底部导入(并且多个JS合并为一个),最好再设置上defer/async
把JS放在顶部导入,并且还能获取到DOM元素对象?♥♥♥
外链资源设置defer/async
设置事件监听:load OR DOMContentLoaded
2. 在渲染解析中,DOM树生成后,等待所有的CSS资源都获取到,GUI再去渲染,生成CSSOM-Tree(CSS样式树)
3. 等待CSSOM-Tree也生成后,会和DOM-Tree合并在一起,生成一个Render-Tree(渲染树)
4. 渲染树包含了页面绘制的具体规则(节点的位置、大小、样式等等信息都有了) => Layout(布局排列),重排(回流)就是重新生成Render-Tree的过程
5. 交给显卡(GPU)开始进行绘制 => Painting(绘制/绘画),重绘就是重新绘制
回流比重绘更消耗性能。触发回流一定会触发重绘,触发重绘不一定触发回流。
重绘 ♥♥♥♥♥
元素样式的改变,但宽高、大小、位置不变,如字体颜色,背景颜色等,会触发页面的重绘。
DOM的重绘:如果节点的位置、大小、结构等样式没有改变,只是改变了一些基础的样式(例如:改变了文字或者背景的颜色),此时无需重新生成Render-Tree,只需要重新Painting绘制即可!
回流 ♥♥♥♥♥
元素的宽高、大小、位置等影响页面布局改变的样式改变,会触发页面的回流。
回流比重绘更消耗性能。 触发回流一定会触发重绘,触发重绘不一定有回流。
DOM的重排(回流):在第一次页面绘制完成后,如果我们修改了页面中节点的“位置、大小、结构等样式”,浏览器需要重新计算绘制规则(也就是重新生成Render-Tree),这个过程就是重排;重新生成Render-Tree后需要重新的绘制;
如何减少回流和重绘
在老版本的浏览器中,我们分别改变了三次样式(都涉及了位置或者大小的改变),会触发三次回流和重绘。
现代浏览器中默认增加了“渲染队列的机制”,以此来减少DOM的回流和重绘。 遇到一行修改样式的代码,先放到渲染队列中,继续看下面一行代码是否还为修改样式的,如果是继续增加到渲染队列中...直到下面的代码不再是修改样式的,而是获取样式的代码!此时不再向渲染队列中增加,把之前渲染队列中要修改的样式一次性渲染到页面中,引发一次DOM的回流和重绘。
性能优化:减少DOM的重排(回流)--♥♥♥♥♥
放弃直接操作DOM,使用vue/react/angular等数据驱动框架
读写分离/样式集中改变:把修改样式和获取样式的操作分离开,避免渲染队列的刷新
3.元素的批量操作:文档碎片、模板字符串拼接等
4.开起GPU加速:
修改“transform”样式,不会引发重排
尽可能修改那些脱离文档流的{例如:position定位,我们修改top/left等信息}:虽然也会引发重排,只是对当前层进行处理,其他层没有改变,不需要重新渲染
5.基于JS实现动画{定时器触发、requestAnimationFrame},我们一般会牺牲平滑度换取速度(性能);但是我们现在实现动画,基本上都是基于CSS3中的transition、animation实现,他们的性能会更好!!
// 老版本三次回流重绘 // 新版本一次回流重绘 box.style.width = '200px'; box.style.height = '200px'; box.style.margin = '20px'; console.log(box.style.width); console.log(box.offsetHeight);复制代码
读写分离
渲染队列机制:在当前上下文中,遇到一行修改样式的代码,浏览器并没有立即渲染,先放到渲染队列里,观察是否还有修改样式的代码,有,同样放进去,直到没有修改样式的代码, 遇到获取样式的操作,就会去刷新渲染队列。
box.style.width = '200px'; console.log(box.style.width); //=>中断渲染队列,立即渲染一次,引发一次DOM回流和重绘 200px box.style.height = '200px'; console.log(box.offsetHeight); box.style.margin = '20px'; // 上面会触发三次 box.style.width = '200px'; box.style.height = '200px'; box.style.margin = '20px'; console.log(box.style.width); console.log(box.offsetHeight);复制代码
集中改变样式
box.className = 'active'; box.style.cssText = 'width:200px;height:200px;'复制代码
在动态操作DOM结构中的优化(例如:数据绑定)
/* for (let i = 1; i <= 5; i++) { let liBox = document.createElement('li'); liBox.innerText = `我是第${i}个LI`; item.appendChild(liBox); //=>每一次向页面中增加,都会触发一次DOM的回流和重绘(5次) } */ // 文档碎片:临时创建的一个存放文档的容器,我们可以把新创建的LI,存放到容器中,当所有的LI都存储完,我们统一把容器中的内容增加到页面中(只触发一次回流) let frag = document.createDocumentFragment(); for (let i = 1; i <= 5; i++) { let liBox = document.createElement('li'); liBox.innerText = `我是第${i}个LI`; frag.appendChild(liBox); } item.appendChild(frag); // 真实项目中,有一个文档碎片类似的方式,也是把要创建的LI事先存储好,最后统一放到页面中渲染(字符串拼接) let str = ``; for (let i = 1; i <= 5; i++) { str += `<li>我是第${i}个LI</li>`; } item.innerHTML = str;复制代码
优化方案:
标签语义化和避免深层次嵌套
CSS选择器渲染是从右到左
尽早尽快地把CSS下载到客户端(充分利用HTTP多请求并发机制)
style
link
@import
放到顶部
避免阻塞的JS加载
async
defer
放到底部
Webkit浏览器预测解析:chrome的预加载扫描器html-preload-scanner通过扫描节点中的 “src” , “link”等属性,找到外部连接资源后进行预加载,避免了资源加载的等待时间,同样实现了提前加载以及加载和执行分离。
减少DOM的回流和重绘
作者:墨林陌
链接:https://juejin.cn/post/7025799428791336968