【使你的页面飞起来】1-渲染优化

340 阅读10分钟

我们来从渲染层面探讨浏览器优化

1. 浏览器渲染原理和关键渲染路径

探究浏览器渲染优化之前,了解浏览器的渲染流程是必要的,如下图所示: image.png

image.png

浏览器的渲染过程主要包括以下几步:

  1. 构建DOM树(DOM tree):从上到下解析HTML文档生成DOM节点树(DOM tree;构建CSSOM(CSS Object Model)树:加载解析样式生成CSSOM树;
  2. 构建渲染树(render tree):根据DOM树和CSSOM树,生成渲染树(render tree);
  3. 渲染树:按顺序展示在屏幕上的一系列矩形,这些矩形带有字体,颜色和尺寸等视觉属性;
  4. 布局(layout):遍历渲染树开始布局,计算每个节点的位置大小信息;
  5. 绘制(painting):遍历渲染树绘制所有节点,为每一个节点适用对应的样式。
  6. Composite(多个层合成)

首先来看几个问题:

  1. DOM树的构建是文档加载完成开始的? 这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。

参考文档:taligarsiel.com/Projects/ho…

  1. 渲染树是在DOM树和CSS样式树构建完毕才开始构建的吗? 这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一边解析,一边渲染的工作现象。

参考文档:www.jianshu.com/p/2d522fc2a…

  1. css的标签嵌套越多,越容易定位到元素 css的解析是自右至左逆向解析的,嵌套越多越增加浏览器的工作量,而不会越快。

原文链接:blog.csdn.net/riddle1981/…

再来看几个渲染阻塞的问题:

  1. 渲染阻塞 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行,然后继续构建DOM。每次去执行JavaScript脚本都会严重地阻塞DOM树的构建。 如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建

所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:

  1. CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
  2. JS置后:我们通常把JS代码放到页面底部,且JavaScript 应尽量少影响 DOM 的构建。

当解析html的时候,会把新来的元素插入dom树里面,同时去查找css,然后把对应的样式规则应用到元素上,查找样式表是按照从右到左的顺序去匹配的。

例如: div p {font-size: 16px},会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)。 所以,我们平时写CSS时,尽量用id和class,千万不要过渡层叠。

  1. css 加载会造成阻塞吗

DOM 和 CSSOM 通常是并行构建的,所以 CSS 加载不会阻塞 DOM 的解析

然而,由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。

因此,CSS 加载会阻塞 Dom 的渲染

由于 JavaScript 是可操纵 DOM 和 css 样式 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。

因此,样式表会在后面的 js 执行前先加载执行完毕,所以css 会阻塞后面 js 的执行

  1. 为什么 JS 阻塞页面加载

由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。

当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

从上面我们可以推理出,由于 GUI 渲染线程与 JavaScript 执行线程是互斥的关系,

当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。

因此如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

推荐文章

追本溯源 浏览器渲染机制
从输入url,到页面渲染都发生了什么
性能优化——关键路径渲染优化

2. 回流与重绘

2.1 新建一个dom的过程

  1. 获取DOM后分割为多个图层
  2. 对每个图层的节点计算样式结果(Recalculate style--样式重计算)
  3. 为每个节点生成图形和位置(Layout--回流和重布局)
  4. 将每个节点绘制填充到图层位图中(Paint Setup和Paint--重绘)
  5. 图层作为纹理上传至GPU
  6. 符合多个图层到页面上生成最终屏幕图像(Composite Layers--图层重组)

2.2 什么是回流、重绘

回流(重排):当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。当页面布局和几何属性改变时就需要回流

重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

我们要注意的是:回流必将引起重绘,而重绘不一定会引起回流

常见的触发回流的属性: image.png

常见的触发重绘的属性: image.png

这里推荐一个网站:csstriggers.com

在控制台观察回流

我们可以通过开发者工具查看load之后是否有layout来判断是否发生了回流

image.png

我们还可以在线实时观察,通过设置该选项,页面重绘时会变为绿色

image.png image.png

2.3 回流的优化点

  1. 用translate替代top改变
  2. 用opacity替代visibility
  3. 不要一条一条地修改 DOM 的样式,预先定义好 class,然后修改 DOM的className
  4. 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
  5. 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量,如offsetHeight,读取会引发回流
  6. 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  7. 动画实现的速度的选择
  8. gif图会频繁触发重绘,但是它在标签中,所以没有独立图层,我们可以将其独立出来图层,这样可以使重绘的范围变小
  9. 对于动画新建图层,但是图层过多也会影响性能,只将需要重绘的部分做图层,其它尽量减少图层,will-change: transform也可以新建一个图层
  10. will-change: transform可以新建一个图层,接下来将要干什么
  11. 启用 GPU 硬件加速

image.png

2.4 创建新图层的方法

  1. 3D或透视变换(perspective transform)CSS属性
  2. 使用加速视频解码的<video>节点,gif图
  3. 拥有3D(WebGL)上下文或加速的2D上下文的<canvas>节点
  4. 混合插件(如Flash)
  5. 对自己的opacity做CSS动画或使用一个动画webkit变换的元素
  6. 拥有加速CSS过滤器的元素
  7. 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)
  8. 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

3. 避免layout thrashing(布局抖动)

  1. 避免回流 这块我们上面已经讲过,这里不再赘述。

  2. 读写分离

我们用代码进行一些读写操作,如下图,其中右上角的红色标记代表该任务过长 image.png

我们对操作进行读写分离:

为什么要读写分离?

多次改变DOM元素的属性,并不会引发多次回流重绘,而是将这些合并为一次,因为浏览器中有缓冲机制。浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。这属于浏览器自身的性能优化

但当你进行读操作时,浏览器会立刻清空队列。因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

所以我们要对读写操作分离,以避免触发多次回流。

document.body.addEventListener('click', function() {
    // Read
    var h1 = element1.clientHeight;
    // Write
    requestAnimationFrame(function() {
        element1.style.height = (h1 * 2) + 'px';
    ));
));
document.body.addEventListener('click', function() {
    // Read
    var h2 = element2.clientHeight;
    // Write
    requestAnimationFrame(function() {
        element2.style.height = (h2 * 2) + 'px';
    ));
));

另外我们可以使用第三方库FastDom来解决页面抖动

4. 复合线程与图层

复合线程(compositor thread)与图层(layers)

复合线程做什么

  • 将页面拆分图层进行绘制再进行复合
  • 利用DevTools了解网页的图层拆分情况
  • 哪些样式仅影响复合

image.png

5. requestIdleCallback,requestIdleCallback

requestIdleCallback与requestAnimationFrame区别,requestAnimationFrame是一帧开始,requestIdleCallback是一帧结束后的空闲时间

如何构建 60FPS 应用 一帧剖析

6. 防抖,节流

7分钟理解JS的节流、防抖及使用场景
防抖debounce 与 节流throttle

7. 长列表优化

长列表:react-window

image.png

参考文章:

从 8 道面试题看浏览器渲染过程与性能优化
(1.6w字)浏览器灵魂之问,请问你能接得住几个?
「浏览器工作原理」写给女友的秘籍-渲染流程篇(1.1W字)
从浏览器渲染原理谈性能优化(2017版)