Browser rendering process analysis

foreword

You may often hear the assertion that CSS animation is better than JS animation, or words such as "hardware acceleration" and "layer promotion"; to understand these content, you need to have a general understanding of the rendering process of the browser , this article is my personal summary of these contents

have to be aware of is:

  1. This article is only a personal learning summary and sorting out. If there are any mistakes or omissions, please correct me
  2. This article takes the Blink kernel of Google Chrome as an example. Most of the reference content links require scientific Internet access.
  3. With the update iteration of Google Chrome, some rendering processes or object nouns may change (for example, RenderObject becomes LayoutObject, RenderLayer becomes PaintLayer), you need to pay attention to the time of the document when viewing related documents

rendering process

Let's take a look at a general rendering process of blink. The source of the picture is Life of a Pixel , a shared slideshow of Google . It comprehensively describes the rendering process of browsing. It is very worth seeing. We will use this picture to sort it out.

rendering process.png

Image sourceLife of a Pixel

The figure is divided into two parts: the rendering process (renderer process) and the GPU process (GPU process), in which the rendering process includes the main thread (main) and the synthesis thread (impl)

We can use the performance tag of Google development tools to see if certain rendering process steps are performed. I wrote a simple html here for comparison

<!DOCTYPE html>
<html lang="zh-cn"><head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>transform demo</title>
</head>
<style>
  #normal {
    display: grid;
    place-items: end;
    width: 150px;
    height: 150px;
    background-color: pink;
  }
​
  #compositor {
    margin-top: 20px;
    display: grid;
    place-items: end;
    width: 150px;
    height: 150px;
    background-color: palegoldenrod;
  }
​
  #stacking {
    display: grid;
    place-items: end;
    width: 150px;
    height: 150px;
    position: absolute;
    z-index: -1;
    top: 240px;
    background-color: skyblue;
  }
​
  .active {
    animation: transformAni 2s both;
  }
​
  @keyframes transformAni {
    to {
      transform: translate(200px);
    }
  }
</style><body>
  <div id="compositor">Compositor Layers</div>
  <div style="display: flex; margin-top: 20px;">
    <div id="cssBtn" style="background-color:  palegoldenrod; width: 200px;">add css animation</div>
  </div>
​
  <div id="stacking">The Stacking Context</div>
  <div style="display: flex; margin-top: 220px;">
    <div id="jsBtn" style="background-color: skyblue; width: 200px;"> add js animation</div>
  </div>
​
  <script>
    const cssBtn = document.getElementById('cssBtn')
    const compositor = document.getElementById('compositor')
    cssBtn.addEventListener('click', () => {
      compositor.classList.add("active");
    })
​
    const jsBtn = document.getElementById('jsBtn')
    const stacking = document.getElementById('stacking')
    jsBtn.addEventListener('click', () => {
      setInterval(() => {
        stacking.style.left = `${stacking.getBoundingClientRect().left + 10}px`
      }, 100);
    })
​
  </script>
</body></html>

performance.png

1. Build the DOM tree

Corresponding to the DOMnode , since the browser itself cannot directly understand and use html, it is necessary to convert the html into a DOM tree that the browser can understand, which is why we can control the dom node through js

DOM tree parsing.pngImage sourceLife of a Pixel

2. Style calculation

对应头图中 style 节点,不仅是html,浏览器同样无法直接读懂我们写的 css 。因此浏览器会将我们写的 css 转换成它能理解的 styleSheets ,同时计算每个 DOM 节点的样式结果。包括将处理样式的继承覆盖,将 rem 等相对单位转换成 px,将 margin: 8 这样的缩写,拆开解析成 margin-left: 8margin-top: 8 等具体的值。可以通过 computed 标签查看。

computed.png

3. 布局计算

对应头图中 layout 节点,这个阶段也是我们很常听到的 回流(reflow),重排。在上两个阶段结束后会生成一个储存其计算结果的树结构 LayoutObject Tree。在这个阶段浏览器会遍历 LayoutObject Tree 计算每个节点在页面上具体的布局(比如是正常流布局,或是flex布局,哪个元素该放到哪个具体的像素位置上),计算文本实际宽高等;这一阶段谷歌正在重构,目前输入和输出都混在 LayoutObject Tree 上,之后可能会将输出部分抽离出来

4. 分层阶段

对应头图中 comp.assign (compositing assignments) 节点,这个阶段是我们获取性能提升的关键。页面上的元素,根据所处坐标空间(基本可以理解为层叠上下文)不同等原因,会被划分为不同的 PaintLayer,通过分层的方式保证页面上元素以正确的顺序层叠;在此基础上,某些特殊的PaintLayer 会被提升为合成层(Compositing Layers),每个合成层拥有单独的 GraphicsLayer , 而没有被提升的 PaintLayer 则与其祖先元素共用同一个 GraphicsLayer.

它们间的对应关系如下图

GraphicsLayer.png 图源 无线性能优化:Composite

每个 GraphicsLayer 都有一个 GraphicsContextGraphicsContext 负责输出该层的位图,即每层代表一份位图,GPU将位图合成渲染到屏幕上也就是我们看到的页面

我们可以通过开发者工具的 Layer 标签看到 GraphicsLayer 的分层,划分 PaintLayer 和 提升为 GraphicsLayer 的条件具体可见 无线性能优化:Composite (需要注意层重叠,层压缩问题)

比如我上面的例子中,我给橙色的 div 加上了 will-change:transform 导致了层提升,而蓝色的 div 与 document 共用一个 GraphicsLayer;我们还可以在 Details 标签看到层提升的具体原因还有内存消耗 (tips: 层提升原因还可以看 safari 浏览器开发者工具的 layers ,会更加具体)

layer.png

5. Pre-paint

这一阶段主要有两个任务,一是判断与上一次paint阶段(见下)相比有哪些内容需要被更新,二是构建 property trees

Paint invalidation which invalidates display items which need to be painted.

Builds paint property trees.

property treesproperty 是指 translation, scale 等需要大量计算的属性。将这些属性抽离出来单独管理,避免父元素的变动导致其子元素上所有的属性都有全部重新计算,具体见 How cc Works

6. paint

绘制阶段,这一阶段即我们常说的重绘阶段,但这一阶段并不是执行实际的页面绘制,而是依据页面内容的层叠顺序生成 绘制任务列表,详见 layer 工具,滚动滑轮可以重播绘制过程,可以观察到,同一层叠上下文情况下,先生成背景绘制任务,再生成元素内容绘制任务,再生成更高层级的层叠上下文元素的绘制任务;

主线程的任务到这里基本结束,将绘制列表提交(commit)到合成线程

7. tiling

tiling 分块,为 GPU光栅化做准备;光栅化是GPU根据绘制任务生成位图,并将位图储存在内存中。大家可能听过 CPU 光栅化的操作,这里引用一段 How cc Works 中文译文

Chromium 目前实际支持三种不同的光栅化和合成的组合方式:软件光栅化 + 软件合成,软件光栅化 + gpu 合成,gpu 光栅化 + gpu 合成。在移动平台上,大部分设备和移动版网页使用的都是 gpu 光栅化 + gpu 合成的渲染方式,理论上性能也最佳

由于这一操作需要消耗较多资源,为了减少资源消耗和使页面更快呈现会将图层进行分块( tiles ),将图块作为光栅化的基本单位,同时优先对视口附近的图块进行光栅化

通过rendering 标签,勾选 layer borders 可以看到分块情况,橙线是不同的 layer 而 青绿色的线则划分了图块

tiling.png

8. raster

这一步由GPU执行光栅化操作,之后的节点我没再深入了解,大概是光栅化生成draw quads 命令,该命令会引用光栅化结果最后将内容展现在屏幕上

总结

最后我们分别录制两个动画的执行流程

js 动画

js-animation-1.png

可以看到 js 动画在每次执行时会重排重绘,执行整个流程,上面橙红色的那条前面有写到 Layout Shift,即 布局提升,也就是我们说的强制重排,因为我们在 js 脚本里执行了 stacking.getBoundingClientRect().left 访问元素位置,这就需要立刻重排来计算元素当前的位置

css动画

css-animation.png

可以看到,css动画主线程上没有进行重排重绘

梳理完整个流程,我们就能理解开头提到的内容了,关键点就在于分层合成

“层提升” 即文中的 分层阶段;

“硬件加速” 即 GPU加速,一些可能导致页面大范围重排重绘(如 translate动画),或需要大量简单计算的任务(如 filter动画)都会导致层提升,将这部分任务交由GPU处理,将处理完后的结果再合成到页面上;

而 css 动画性能更优的原因是:

  1. 避免了通过js访问元素的位置信息导致强制重排
  2. css动画元素移动时在合成层上进行,避免了页面重排
  3. 合成由 GPU 进程控制,即使 js 阻塞主线程,css动画也能正常执行

层提升会加大内存消耗,加大移动端设备负担,需要酌情使用

补充

will-change

上文我们的例子提到了 will-change 属性,它的作用是提前告知浏览器可能变动的属性,让浏览器提前做好准备,提前进行相关计算等,它有以下取值

  • auto 让浏览器自己猜哪些值会变动
  • scroll-position 表示滚动条位置可能发生变化或产生动画
  • contents 表示元素内容可能变动或产生动画
  • <custom-ident> 表示所有css属性

基本上哪里的css属性变化导致了页面的卡顿都可以使用 will-change 优化

我们的例子中已经写入了 will-change: transform ,因此浏览器一开始就帮我们做了层提升准备,所以橙色 div 一开始在页面上就是分层的情况。而如果我们去掉这个属性,观察 layer 会发现橙色 div 一开始在页面上并没有层提升,只有在执行动画时才进行了层提升,动画结束后层提升又消失了

使用该属性同样要注意的是内存消耗问题,因为浏览器会提前进行优化计算并储存计算结果。由于浏览器本身已经做了十足的性能优化,因此在页面没出现动画卡顿之前没有必要使用该属性,如果需要使用也尽量通过以下形式:

.will-change-parent:hover .will-change {
  will-change: transform;
}
.will-change {
  transition: transform 0.3s;
}
.will-change:hover {
  transform: scale(1.5);
}

当父元素 hover 时,给子元素加上 will-change,hover 失效则移出,既给了浏览器准备的时间,又避免了一直挂着该属性带来的资源消耗

requestAnimationFrame / requestIdleCallback

When it comes to animation, we will mention it by the way requestAnimationFrameandrequestIdleCallback

The animations we see are all composed of a series of coherent pictures that are quickly played on the screen. In order to prevent the eyes from feeling stuck, the refresh rate of most screens is 60Hz, that is, the screen is refreshed sixty times a second. Refresh is called a frame, and the time of one frame is about 16.7ms. If the rendering time of one frame exceeds this number, it will cause the animation to appear stuck. The process of one frame is roughly as follows

anatomy-of-a-frame.png

Source : The Anatomy of a Frame

requestAnimationFrameIt will be executed once before the rendering process of each frame is executed, so when using js to achieve animation, it setIntervalis requestAnimationFramemore reliable than the uncertainty of the actual execution time;

And requestIdleCallbackit is to judge whether there is any remaining time before the end of each frame, if there is, it will be executed, if not, it will not be executed

Reference link

  1. Life of a Pixel
  2. chromium renderer/core/paint
  3. Wireless performance optimization: Composite
  4. How cc Works / Chinese
  5. How Blink works / Chinese
  6. RenderingNG deep-dive: BlinkNG / Chinese
  7. The Anatomy of a Frame
  8. "CSS New World"

Guess you like

Origin juejin.im/post/7116819495628472327