浏览器渲染机制

2022/03/10 Web 共 1942 字,约 6 分钟

浏览器的渲染机制一般分为以下几个步骤:

    1. 处理 HTML 并构建 DOM 树
    1. 处理 CSS 构建 CSSOM 树
    1. 将 DOM 树与 CSSOM 树合并成一个渲染树
    1. 根据渲染树来布局,计算每个节点的位置
    1. 调用 GPU 绘制,合成图层,显示在屏幕上

CSSOM

在构建 CSSOM 树时,会阻塞渲染,直到 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢

当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM

Load 和 DOMContentLoaded 区别

Load 事件触发代表页面中的 DOM、CSS、JS、图片已经全部加载完毕

DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS、JS、图片加载

图层

一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用

通过以下几个常用的属性可以生成新图层:

  • 3D 变换:translate3dtranslate2d
  • will-change
  • videoiframe标签
  • 通过动画实现的 opacity 动画转换
  • position: fixed

重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大

  • 重绘是当节点需要更改外观而不会影响布局,比如 color 属性值变更
  • 回流是布局或几何属性需要改变

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流

所以以下几个动作可能会导致性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或浮动
  • 盒模型

重绘和回流与 EventLoop 有关:

    1. 当 EventLoop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次
    1. 然后判断是否有 resize 或者 scroll,有的话会去触发事件,所以 resizescroll 事件也是至少 16ms 才会触发一次,并且自带节流功能
    1. 判断是否触发了 media query
    1. 更新动画并且发送事件
    1. 判断是否有全屏操作事件
    1. 执行 requestAnimationFrame 回调
    1. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载,但是兼容性不好
    1. 更新界面
  • 9.以上就是一帧中可能会做的事。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调

减少重绘与回流

  • 使用 trasnlate 替代 top

    <div class="block"></div>
    <style>
      .block {
        position: absolute;
        top: 50px;
        height: 100px;
        width: 100px;
        background-color: blue;
      }
    </style>
    <script>
      setTimeout(() => {
        // 引起回流
        document.querySelector('.block').style.top = '100px'
      })
    </script>
    
  • 使用 visibility 替换 display: none,因为前者只会引起重绘,后者会引发回流
  • 把 DOM 离线后修改。比如:先把 DOM 给 display: none (有一次 Reflow),然后进行修改,修改完成后再把它显示出来
  • 不要把 DOM 节点的属性值放在一个循环里当成循环里的变量

    for(let i = 0; i < 50; i++) {
      // 获取 offsetTop 会导致回流,因为需要去获取正确的值
      console.log(document.querySelector('.block').style.offsetTop)
    }
    
  • 避免使用 table 布局,可能很小的改动都会造成整个 table 重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深
  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层

参考文章:

  • https://html.spec.whatwg.org/multipage/webappapis.html#webappapis