浏览器渲染流程解析

前言

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

需要注意的是:

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

渲染流程

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

图源 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>
![computed.png](https://upload-images.jianshu.io/upload_images/18214510-d275a5ddc6594ee9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

1. 构建DOM树

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

图源 Life of a Pixel

2. 样式计算

对应头图中 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.

它们间的对应关系如下图

图源 无线性能优化: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.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

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

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

图源 The Anatomy of a Frame

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

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

参考链接

  1. Life of a Pixel

  2. chromium renderer/core/paint

  3. 无线性能优化:Composite

  4. How cc Works / 中文

  5. How Blink works / 中文

  6. RenderingNG deep-dive: BlinkNG / 中文

  7. The Anatomy of a Frame

  8. 《css新世界》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,151评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,465评论 3 405
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,429评论 0 368
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,147评论 1 301
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,149评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,614评论 1 315
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,965评论 3 429
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,950评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,486评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,524评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,640评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,228评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,976评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,407评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,552评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,215评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,714评论 2 366

推荐阅读更多精彩内容