技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# 浏览器渲染的过程

# 页面渲染的整体过程

  • JavaScript:包含与视觉变化效果相关的js 操作。包括并不限于:dom 更新、元素样式动态改变、jQuery 的 animate 函数等。
  • Style:样式计算。这个过程,浏览器根据 css 选择器计算哪些元素应该应用哪些规则,然后将样式规则落实到每个元素上去,确定每个元素具体的样式。
  • Layout:布局。在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。
  • Painting:绘制。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。(paint 和 draw 的区别:paint 是把内容填充到页面,而 draw 是把页面反映到屏幕上)
  • Composite:合成。由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

# 页面渲染的详细图

红色部分表示重绘或者回流

  1. Vsync 接收到 Vsync 信号,这一帧开始

  2. Input event handlers 之前 Compositor Thread 接收到的用户 UI 交互输入在这一刻会被传入给主线程,触发相关 event 的回调。

    All input event handlers (touchmove, scroll, click) should fire first, once per frame, but that’s not necessarily the case; a scheduler makes best-effort attempts, the success of which varies between Operating Systems.

    这意味着,尽管 Compositor Thread 能在 16ms 内接收到 OS 传来的多次输入,但是触发相应事件、传入到主线程被 JS 感知却是每帧一次,甚至可能低于每帧一次。也就是说 touchmove、mousemove 等事件最快也就每帧执行一次,所以自带了相对于动画的节流效果!如果你的主线程有动画之类的卡了一点,事件触发频率非常可能低于 16ms。我在最开始关于渲染时机的内容中说了 scroll 和 resize 因为和渲染处于同一轮次,所以最快也就每帧执行一次,现在来看,不仅仅是 scroll 和 resize!连 touchmove、mousemove 等事件,由于 Compositor Thread 的机制原因,也依然如此详见这个 jsfiddle (opens new window),大家可以试试,你可以发现 mousemove 回调和 requestAnimationFrame 回调的调用频率是完全一致的,mousemove 的执行次数跟 raf 执行次数一模一样,永远没有任何一次出现 mousemove 执行两次而 rAF 还没有执行一次的情况发生。另外两次执行间隔在 14 到 20 毫秒之间,主要是因为帧的间隔不会精确到 16.666 毫秒哈,基本是 14ms~20ms 之间大致波动的,大家可以打开 timeline 观察。另外有个挺奇怪的现象是每次鼠标从 devtool 移回页面区域里的时候,会非常快的触发两次 mousemove(间隔有时小于 5ms),虽然依然每次 mousemove 后依然紧跟 raf,这意味着非常快速的触发了两帧。

  3. requestAnimationFrame 图中的红线的意思是你可能会在 JS 里Force Layout (opens new window),也就是我们说的访问了 scrollWidth、clientHeight、ComputedStyle 等触发了强制重排,导致 Recalc Styles 和 Layout 前移到代码执行过程当中。

  4. parse HTML 如果有 DOM 变动,那么会有解析 DOM 的这一过程。

  5. Recalc Styles 如果你在 JS 执行过程中修改了样式或者改动了 DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式。

  6. Layout 我们常说的重排 reflow。如果有涉及元素位置信息的 DOM 改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。而单纯修改 color、background 等等则不会触发重排。详见css-triggers (opens new window)

  7. update layer tree 这一步实际是更新 Render Layer 的层叠排序关系,也就是我们之前说的为了搞定层叠上下文搞出的那个东西,因为之前更新了相关样式信息和重排,所以层叠情况也可能变动。

  8. Paint 其实 Paint 有两步,第一步是记录要执行哪些绘画调用,第二步才是执行这些绘画调用。第一步只是把所需要进行的操作记录序列化进一个叫做 SkPicture 的数据结构里:

    The SkPicture is a serializable data structure that can capture and then later replay commands, similar to a display list (opens new window).

    这个 SkPicture 其实就一个列表,记录了你的 commands。接下来的第二步里会将 SkPicture 中的操作 replay 出来,这里才是将这些操作真正执行:光栅化和填充进位图。主线程中和我们在 Timeline 中看到的这个 Paint 其实是 Paint 的第一步操作。第二步是后续的 Rasterize 步骤(见后文)。

  9. Composite 主线程里的这一步会计算出每个 Graphics Layers 的合成时所需要的 data,包括位移(Translation)、缩放(Scale)、旋转(Rotation)、Alpha 混合等操作的参数,并把这些内容传给 Compositor Thread,然后就是图中我们看到的第一个 commit:Main Thread 告诉 Compositor Thread,我搞定了,你接手吧。然后主线程此时会去执行 requestIdleCallback。这一步并没有真正对 Graphics Layers 完成位图的 composite。

  10. Raster Scheduled and Rasterize 第 8 步生成的 SkPicture records 在这个阶段被执行。

    SkPicture records on the compositor thread get turned into bitmaps on the GPU in one of two ways: either painted by Skia’s software rasterizer into a bitmap and uploaded to the GPU as a texture, or painted by Skia’s OpenGL backend (Ganesh) directly into textures on the GPU.

    可以看出Rasterization其实有两种形式:

    • 一种是基于CPU、使用Skia库的Software Rasterization,首先绘制进位图里,然后再作为纹理上传至GPU。这一方式中,Compositor Thread会spawn出一个或多个Compositor Tile Worker Thread,然后多线程并行执行SkPicture records中的绘画操作,以之前介绍的Graphics Layer为单位,绘制Graphics Layer里的Render Object。同时这一过程是将Layer拆分为多个小tile进行光栅化后写入进tile对应的位图中的。
    • 另一种则是基于GPU的Hardware Rasterization,也是基于Compositor Tile Worker Thread,也是分tile进行,但是这个过程不是像Software Rasterization那样在CPU里绘制到位图里,然后再上传到GPU中作为纹理。而是借助Skia’s OpenGL backend (Ganesh) 直接在GPU中的纹理中进行绘画和光栅化,填充像素。也就是我们常说的GPU Raster。

    现在基本最新版的几大浏览器都是硬件Rasterization了,但是对于一些移动端基本还是Software Rasterization较多。打开你的chrome浏览器输入chrome://gpu/ 可以看看你的chrome的GPU加速情况。下图是我的: 使用Hardware Rasterization的好处在于:以往Software Rasterization的方式,受限于CPU和GPU之前的上传带宽,把位图从RAM里上传到GPU的VRAM里的过程是有不可忽视的性能开销的。若Rasterization的区域较大,那么使用Software Rasterization很可能在这里出现卡顿。下面这个例子是Chrome32和Chrome41的对比,后者的版本实现了Hardware Rasterization。

    不过,对于图片、canvas等情况,我没有查到到底是怎么处理的,但是我觉得绝对是有一个从CPU上传到GPU的过程的,所以应该有一些情况不是纯Hardware Rasterization的,两者应该是结合使用的。另外就是硬件还是软件Rasterization主要还是由设备决定的,在这个地方并没有我们手动优化的空间,但是这里涉及到一些后面的内容,所以简单介绍了一下。

  11. commit 如果是 Software Rasterization,所有 tile 的光栅化完成后 Compositor Thread 会 commit 通知 GPU Thread,于是所有的 tile 的位图都会作为纹理都会被 GPU Thread 上传到 GPU 里。如果是使用 GPU 的 Hardware Rasterization,那么此时纹理都已经在 GPU 中。接下来,GPU Thread 会调用平台对应的 3D API(windows 下是D3D (opens new window),其他平台都是 GL),把所有纹理绘制到最终的一个位图里,从而完成纹理的合并。 同时,非常关键的一点:在纹理的合并时,借助于 3D API 的相关合成参数,可以在合并前对纹理 transformations(也就是之前提到的位移、旋转、缩放、alpha 通道改变等等操作),先变形再合并。合并完成之后就可以将内容呈现到屏幕上了。

# Render Layer 也会影响页面的渲染进程

从DOM Tree到Layer Tree会经过这样的过程:

Layers是浏览器在绘制过程中生成的一个层。因为浏览器底层渲染的本质是纵向分层、横向分块

1.一些属性会让元素们创建出不同的渲染层

  • 有明确的定位属性(relative、fixed、sticky、absolute)
  • 透明的(opacity 小于 1)
  • 有 CSS 滤镜(fliter)
  • 有 CSS transform 属性(不为 none)
  • ...

2.达成一些条件,渲染层会提升为合成层

  • 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)
  • 3D 或者 硬件加速的 2D Canvas 元素
  • video 元素
  • 有 3D transform
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
  • ...
【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM