记录–浏览器渲染流程解析

  • 记录–浏览器渲染流程解析已关闭评论
  • 107 次浏览
  • A+
所属分类:Web前端
摘要

大家可能经常会听到 css 动画比 js动画性能更好这样的论断,或者是“硬件加速”,“层提升” 这样的字眼;要了解这些内容就需要对浏览器的渲染流程有个大致的了解,本文就是我个人对这些内容的一个总结梳理


这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

记录--浏览器渲染流程解析

大家可能经常会听到 css 动画比 js动画性能更好这样的论断,或者是“硬件加速”,“层提升” 这样的字眼;要了解这些内容就需要对浏览器的渲染流程有个大致的了解,本文就是我个人对这些内容的一个总结梳理

需要注意的是:

  1. 本文仅个人学习总结梳理,如有错漏,望指正
  2. 本文以谷歌浏览器Blink内核为例
  3. 随着谷歌浏览器的更新迭代,有些渲染流程或对象名词可能发生变化(如, RenderObject 变成了 LayoutObject,RenderLayer 变成了 PaintLayer),查看相关文档时需要注意文档的时间

渲染流程

先来看下blink的一个大致渲染流程,图源谷歌的一份共享幻灯片 Life of a Pixel ,它比较全面的阐述了浏览的渲染流程,非常值得一看,我们就借这张图来梳理一遍

记录--浏览器渲染流程解析

图中分为 渲染进程(renderer process) 和 GPU进程(GPU process) 两部分,其中渲染进程包含 主线程(main) 和 合成线程(impl)

我们可以借助谷歌开发工具的 performance 标签查看是否执行了某些渲染流程步骤,我这里写了一个简单的html可以作为对比

<!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>

记录--浏览器渲染流程解析

1. 构建DOM树

对应头图中 DOM 节点,由于浏览器本身无法直接理解和使用html,所以需要将html转换为浏览器能够理解的DOM树,也正是因此我们才能通过js控制dom节点

记录--浏览器渲染流程解析

2. 样式计算

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

记录--浏览器渲染流程解析

3. 布局计算

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

4. 分层阶段

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

它们间的对应关系如下图

记录--浏览器渲染流程解析

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

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

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

记录--浏览器渲染流程解析

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 而 青绿色的线则划分了图块

记录--浏览器渲染流程解析

8. raster

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

总结

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

js 动画

 记录--浏览器渲染流程解析

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

css动画

记录--浏览器渲染流程解析

可以看到,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

讲到动画我们就顺便提一嘴 requestAnimationFramerequestIdleCallback

我们看到的动画都是由屏幕快速播放一系列连贯的图片组成,为了让人眼感受不到卡顿,大多数屏幕的刷新频率都是60Hz,即一秒钟刷新六十次屏幕,每次刷新叫做一帧,一帧时间大约16.7ms,如果一帧的渲染时间超过这个数就会导致动画看起来出现了卡顿,一帧流程大致如下图

记录--浏览器渲染流程解析

requestAnimationFrame会在每一帧的渲染流程执行前都执行一次,因此使用js实现动画时,相比于 setInterval 实际执行时间的不确定性requestAnimationFrame 更加可靠;

requestIdleCallback 则是在每一帧结束前判断是否有剩余时间,如果有则执行,无则不执行

本文转载于:

https://juejin.cn/post/7116819495628472327

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 记录--浏览器渲染流程解析