阅读 126

浏览器的底层渲染机制

【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的重排(回流)--♥♥♥♥♥

  1. 放弃直接操作DOM,使用vue/react/angular等数据驱动框架

  2. 读写分离/样式集中改变:把修改样式和获取样式的操作分离开,避免渲染队列的刷新

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


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐