浏览器渲染过程及优化

153 阅读6分钟

前言

经过这几天的面试以及个人的兴趣,去了解了下浏览器渲染的过程,以下内容是观看了大量文章得来的总结,并未亲自实现,如有错误欢迎指出

前置知识

什么是回流?是什么重绘?

回流,也叫重排,当我们修改页面 DOM 结构(插入节点,删除节点)或者修改节点的宽,高等会引起页面节点布局位置的变化时,浏览器需要重新计算各个节点在页面中的位置情况,需要重新构建 DOM 树来实现新的布局,这个过程就叫回流。

重绘,当页面的布局没有改变,而只是修改页面的样式,不需要重新计算页面的布局时,页面只需要重新进行渲染,这个过程叫做重绘。

它们两者的关系是:回流一定会引起重绘,但是重绘不一定会引起回流。回流的性能比重绘的性能差,所以在开发过程中要尽量避免回流。

浏览器的渲染过程

image.png

构建过程:CPU会开辟一个栈内存空间来解析代码,同时浏览器会分配一个主线程来解析HTML,首先对 HTML 标签进行解析,将开始标签压入一个栈中,当解析到结束标签时,就会弹出栈顶元素,将该元素以及内容挂载到 DOM 树上;解析 HTML 页面构建 DOM 树相当于树的深度优先遍历,会先把一个节点的所有子节点都解析完,再去解析兄弟节点。

在解析过程中,如果遇到 link 标签请求外部 CSS 资源,浏览器会开辟一块新的内存空间(STACK QUEUE),并创建一个新的线程去请求资源,浏览器的主线程继续解析页面代码,不会被堵塞。如果遇到像 img, vedio, audio 等请求外部资源的标签,也会在新创建的内存空间中等待请求,不会打断页面的渲染。

若遇到 script 标签,则会打断 DOM 树的构建,执行 script 中的 JS 代码,所以为了提高用户的体验,建议将 script 标签放在 body 的结束标签之前。这里需要注意请求外部资源的 script 标签有 defer 和 async 属性,async 是异步加载外部资源,当下载完毕之后会打断页面的渲染,执行下载好的 JS 代码,如果有多个 script 标签添加了 async 属性,那它们的执行顺序是不确定的;defer 也会异步加载外部资源,但是它会延迟执行,等页面解析渲染完毕之后才会去执行下载好的 JS 代码。

image.png

当浏览器的主线程构建完 DOM 树之后,会去查看分线程的 CSS 文件是否请求完毕,请求完毕之后会根据 CSS 文件构建 CSSOM(CSS Object Model) 树。

将 DOM 树和 CSSOM 树进行一个合并生成渲染树,这个时候会通过选择器把样式添加到对应的节点上,并将 DOM 树中一些不会渲染在页面上的标签进行删除,例如:head,link,script 标签等,此时的渲染树是可以看出页面的一个整体结构的。

构建完渲染树之后,线程会根据渲染树进行布局(Layout),我们需要知道每一个节点在页面上的位置,布局(Layout)其实就是找到所有元素的几何关系的过程。主线程会遍历 DOM 及相关元素的计算样式,构建出包含每个元素的页面坐标信息及盒子模型大小的布局树(Render Tree),遍历过程中,会跳过隐藏的元素(display: none),另外,伪元素虽然在 DOM 上不可见,但是在布局树上是可见的;随后进行绘制(Painting)。

从绘制(Painting)完成到展示页面(display),中间还有一个合成(Compositing)的过程。浏览器的渲染过程并不是只有一个图层,每个渲染节点就是一个图层,当它们绘制完成之后,会根据一定的先后顺序对这个图层进行合并,对于相同位置具有多个元素的,会着重考虑它们的重叠顺序,以保证页面的正确渲染,在该图层中修改样式可能会引起回流或者重绘。

为何需要性能优化

对于修改页面 DOM 结构以及样式等行为,会引起页面的回流或重绘,而频繁修改 DOM 结构造成回流重绘,浏览器的性能就会变得比较低,所以需要通过一些手段来避免浏览器频繁的进行回流和重绘。

优化的方法

1.读写分离

新版浏览器对于修改 DOM 操作,都有一个处理方法,当执行完一行操作页面的语句之后,不会马上去更新页面,浏览器会创建一个任务队列,将该操作进行一个缓存,然后继续读取下一行代码,如果下一行代码还是操作页面的语句,则会刷新队列,将该操作也进行缓存,一直往下读取,直到下一行代码不是操作页面的语句,再去修改页面。我们可以尽量把修改页面的语句放在一起,这样可以避免浏览器多次修改页面造成多次回流或重绘。

let box = document.getElementById('box');
box.style.width = '100px';
box.style.height = '100px';
// 把操作 DOM 的语句写在一起,只会造成一次回流
let box = document.getElementById('box');
box.style.width = '100px';
console.log(1);
box.style.height = '100px';
// 由于 console.log() 把操作 DOM 的语句分开了,该代码会造成两次回流

2.借助文档碎片或模板字符串减少对页面的操作

let box = document.getElementById('box');
let frg = document.createDocumentFragment();
for(let i=0; i < 5; i++) {
      let li = document.createElement('li')
      li.innerHTML = i
      frg.appendChild(li)
}
box.appendChild(frg)
frg = null
// 此处使用文档碎片缓存各个节点,最后再插入页面,页面只造成一次回流,避免了逐一插入造成多次回流
let box = document.getElementById('box');
let str = ''
for(let i=0; i < 5; i++) {
      str += `<li>${i}</li>`
}
box.innerHTML = str
// 此处使用模板字符串进行拼接,最后插入页面,原理与文档碎片相同

3.开启硬件加速(GPU加速)

原理:开启GPU加速,会重新创建一个图层,将开启加速的元素添加到该图层,该元素在新增的图层进行样式修改等行为,不会引起页面的回流与重绘。可以用来解决动画效果导致页面不断回流重绘导致动画掉帧的问题。

开启方式: 1.transform属性值可以自己设置,将图形转换为3D。 2.opciaty属性搭配动画使用。 3.will-change属性

注意:不是开启的图层越多越好,开启图层需要消耗内存资源,如果开启过多的图层,反而会使页面渲染效率降低。所以在编写CSS代码的时候要慎重考虑为哪些元素开启GPU加速。