《Life of a Pixel》——浏览器渲染流程概要

本文是 Chrome 团队新人入职学习资料《Life of a Pixel》的概要版,首发于个人博客( 点此查看),欢迎关注。
原文 Slides 地址: https://bit.ly/lifeofapixel
中文字幕演讲视频地址: https://www.bilibili.com/vide...

《Life of a Pixel》内容讲的是开发者编写的 web 内容(也就是一般所说的 HTML+CSS+JS 以及 image、video 等其余资源)渲染为图形并呈现到屏幕上的整个过程。我将其演讲内容分为如下三个部分,第一个是静态渲染过程,讲述一个完整的从 content 到 pixel 的渲染过程;第二个是动态更新过程,讲述浏览器如何高效更新页面内容。前端

概览


首先看一下整个过程的概览。在了解详细内容前,咱们也大概知道浏览器最终是经过调用 GPU 完成像素到屏幕的绘制。但这个过程当中有不少的步骤。注意概览图中浏览器的渲染进程是放在沙箱进程中由 Blink 处理的,这也是其安全策略。git

静态渲染过程


这一页的内容对于广大前端从业者来讲应该都比较熟悉。首先是 HTML 经过 HTMLDocumentParser 转换为 DOM 树,CSS 经过 CSSParser 转换为 StyleRule 集。每一个 StyleRule 包含 CSSSelector 和 CSSPropertyValue,固然两者间存在对应关系。再加上浏览器提供的每种类型元素的 DefaultStyle,通过一系列的计算(这一步称为 recalc)生成全部元素包含全部 style 属性值的 ComputedStyle,如右上角的图所示。ComputedStyle 也经过开发者工具和 JS API 暴露了出来,相信你们也不陌生。web


接下来一步就是 layout。layout 的功能是根据上一步获得的全部元素的 computedStyle,将全部元素的位置布局计算好。每一个元素在这一步会生成一个 LayoutObject,简单来讲其包含四个属性:x、y、width 及 height 用于标识其布局位置。layout 最简单的状况就是,全部的块按照 DOM 顺序从上往下排列,也就是咱们常说的流。layout 也包含很复杂的状况,好比带有 overflow 属性的元素,浏览器会计算其 border-box 的长宽和实际内容的长宽。若是设置为 scroll 而且内容超出,还要为其预留滚动条的位置。此外, float、flexbox 等布局也会使得 layout 变复杂。。因此为了解决复杂性的问题,layout 阶段浏览器首先会生成一个和 DOM 树节点大体一一对应的 layout 树,而后遍历该树,将通过计算后得出的位置布局数据填入节点。对于这个过程,Chrome 团队认为没有很好地分离输入和输出,所以下一代的 layout 系统会进行重构,使得分层更加清晰。api


而后进入 paint 阶段。须要注意的是这一步并非真的绘制,只是生成对应的指令。对于每一个 LayoutObject,浏览器会生成一个列表,列表中的每一项记录着绘制指令(好比画个红色的矩形)。记住这个待绘制列表项,后面会出现不少次。绘制按照堆栈也就是 z 轴的顺序在多个阶段进行。每一个阶段只根据当前元素对应的属性(background->floats->foregrounds->outlines)进行绘制。注意,绘制并不严格是按照上述四种元素属性顺序,此处只做举例说明。浏览器


下面就进入 raster 阶段,中文名为栅格化。栅格化的操做将上一步 paint 阶段每一个 LayoutObject 存储的绘制指令列表中的每一项转换为颜色值的位图。位图中的每一项存储着 RGBA 值,对应着一个像素。位图存在于 GPU 内存中,尚未显示到屏幕上。GPU 除了用来存位图信息,还能执行生成位图的命令,也就是说栅格化过程可经过 GPU 进行,Chrome 默认开启 GPU 栅格化。GPU 栅格化的过程以下:浏览器调用 Skia 库,Skia 库对绘制指令创建单独的缓冲区以进行指令的转译处理,这一过程结束后缓冲区内容被释放输出并生成 OpenGL 调用。至此,这些 OpenGL 调用还存在于渲染沙箱进程,须要经过命令缓冲区机制代理传输到 GPU 进程执行。使用 GPU 进程的缘由一是须要绕过渲染器沙箱的限制,二是将 OpenGL 程序若是不稳定或有安全漏洞,隔离开使其不至于影响浏览器的稳定性。在将来演进上,栅格化处理将转移至 GPU 进程中进行,以提高性能。同时 Vulkan 也会被支持。(注:Skia 是一个独立的图形处理函数库,其对硬件作了一层抽象,能够执行一系列相对底层 OpenGL 更复杂的指令。OpenGL 是跨语言跨平台的系统级绘图API。Vulkan 是下一代的绘图 API,旨在替代 OpenGL。)安全

以上过程揭示了静态渲染,也就是从 web content 到内存中的像素的整个流程。可是实际过程当中页面是不断更新的,包括滚动、动画、js 等都会改变页面内容。一个完整的渲染过程是很昂贵的,如何高效更新也是讨论的重点。ide

动态更新过程


首先明确一个概念,帧。涉及到时间时,每一帧是当前 Web 内容的完整呈现,一般,若是每秒低于 60 帧,滚动和动画就会显得有些卡顿。函数


第一个优化方向最容易想到,即跟踪改变的部分,复用没有改变的部分。所以针对第一部分提到的 style、layout、paint、raster,浏览器都作了精细化跟踪失效的处理,每一帧都会复用前一帧没有变化的部分,只有被标记了须要变动的部分才会进行从新处理。工具


因为 JS 和渲染都存在于主线程中,所以若是 JS 占据主线程作了耗时的操做,即便渲染很快,页面看起来仍然是比较卡顿的。因此这又引出了下一个优化点,compositing,中文名合成。布局


合成包含两个概念,一是将页面分解成多个 layer,二是将这些 layer 在另外一个线程中合成。layer 相似 PS 中图层的概念,能够独立于其余 layer 进行变换和栅格化。开发者工具中对其也有直观的展现。合成线程须要可以处理用户可能致使页面发生变化的输入事件好比(变换、剪切、滚动、特效),由于这些操做涉及了复合图层的改变。这样能够和主线程执行 js 互不干扰。可是当合成线程没法处理某个输入事件时,仍是会由主线程来处理。layer 的存储依然是经过树形结构实现。合成更新是新出现的生命周期,出如今 layout 以后 paint 以前。每一个 layer 都被单独绘制,所以其也有属于本身的绘制指令列表。将来,Chrome 可能会将合成图层生命周期放到 paint 后面。


主线程的绘制阶段完成后,主线程上的 layer tree 将会被复制到合成线程上,合成完毕后再返回主线程。整个过程相似 git 中分支代码的合并。


合成线程中,在对图层进行栅格化以前,还会有一步 tiling 的操做,也就是将 layer 拆分为多个小图块(tile),目的是为了防止出现某些状况下,某个滚动 layer 很长,但实际只须要展现当前容器内的一小块,若是整个 layer 进行栅格化将会比较浪费资源。复杂管理分块的模块叫 tile manager,它会随着滚动区域的变化,优先建立相邻的图块。全部图块栅格化完成后,合成线程将绘制 quads(四边形绘制)。一个 quad 相似于在屏幕上绘制一个图块的指令,其引用在内存中生成的栅格图块,而后被封装,由渲染进程提交到浏览器进程,这些就是每一个动画帧。


这里为了实现能够一边能够执行前一个提交的图块绘制任务,一边继续等待新的任务,合成线程还作了一些优化,实现了一个 pending layer tree。其接收 commit,当其准备好绘制后,会被激活(activation)从而复制到 active layer tree 上进行绘制任务。


浏览器拿到渲染进程发来的动画帧以后,结合非内容区的其余渲染进程(好比浏览器 UI),调用 OpenGL 指令绘制最终的画面。

总结


最后仍是这张图,快速过一下每一个步骤,web 内容、生成 DOM 树、解决样式问题、更新布局、生成合成图层、把图层绘制到待显示项列表中、把图层树提交给合成线程、把图层切分为小图块、对图块进行栅格化操做、把 pending layer tree 复制到 active layer tree、把树绘制成 quads、提交 quad 到浏览器进程、经过 GPU 进程调用 GL 指令绘制像素至屏幕上。

以上,就是一个像素的一辈子奇妙之旅。

相关文章
相关标签/搜索