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


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

    大家可能经常会听到 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;
         150px;
        height: 150px;
        background-color: pink;
      }
    ​
      #compositor {
        margin-top: 20px;
        display: grid;
        place-items: end;
         150px;
        height: 150px;
        background-color: palegoldenrod;
      }
    ​
      #stacking {
        display: grid;
        place-items: end;
         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;  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;  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

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

     

  • 相关阅读:
    自定义asp.net mvc Filter 过滤器
    基于委托的C#异步编程的一个小例子 带有回调函数的例子
    ASCII、Unicode和UTF-8编码的区别
    Specification模式的一个不错的示例代码
    codesmith 自动生成C# model 实体模板
    Quartz.NET 实现定时任务调度
    FtpHelper类匿名获取FTP文件
    crc32 根据字符串获取校验值
    机器学习能做什么
    RunHelper,一个为跑步而设计的开源的android app
  • 原文地址:https://www.cnblogs.com/smileZAZ/p/16458533.html
Copyright © 2020-2023  润新知