浏览器页面渲染原理

本文以 Chrome 浏览器为例理解现代化浏览器渲染原理

现代化浏览器多进程架构中,浏览器会进程创建一个沙箱的渲染进程,并使用 Web 渲染引擎(如 Blink)将 HTML、CSS 和 JavaScript 等 Web 内容转换为用户可以与之交互的网页。

Chrome 的 Blink 渲染进程主要包含以下等线程:

  • 主线程(Blink 渲染引擎)
  • 合成线程 compositor ("cc")
  • WebWorker 线程
  • 光栅线程

渲染管道

一个页面形成需要经过多个流程阶段,我们把这样的一个处理流程叫做渲染流水线或者渲染管道。为了提高渲染效率,整个渲染渲染流程就像是一条管道,分成多个渲染阶段,每个阶段都会产生中间产物,当发生更新时,就可以从某一阶段的产物复用开始,这样即提高渲染效率并且分多阶段任务执行,可以降低系统复杂度,提高任务调度灵活性。

管道的最终是将 Web 内容转换成底层操作系统提供的图形库调用操作,去驱动显卡生成页面像素,在多平台的今天,有标准 API 图形库 OpenGL,但在 window 平台,还需要额外转换成 DirectX API 调用,未来还将实现更多图形库支持,如 vulkan。

渲染流程

  • Parse HTML:解析 HTML,构建 DOM 对象
    • 预加载扫描器:预加载扫描程序会查看 HTML 解析器生成的令牌,并将请求发送到浏览器进程中的网络线程
  • Recalculate Style:样式计算
  • Layout:布局计算
  • Layers:图层分层
  • Panit:图层绘制
    • Tile 分块
  • Raster:光栅
  • Compositing:合成
  • GPU display

Layout:布局计算

  1. 创建布局树(Layout Tree),遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中。

    DOM 并不是跟 layout tree 的节点并不是一对一对应关系,不可见的节点包括:
    • display:none 的元素
    • 伪元素 after 与 before
    • 文本内联元素将会被隐式 block 包裹
    • head 标签下面的全部内容
  2. 布局计算,计算每个节点的布局信息,比如坐标及几何等信息。

Layers:分层

浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。

如果没有采用分层机制,那么每次页面有很小的变化时,都从布局树直接生成目标图片的话,会严重影响页面的渲染效率。

为了提升每帧的渲染效率,Chrome 引入了分层和合成的机制。

上图中从 DOM 到最终 Graphic Layer(图层)的转换并不是一对一,只有具有特定样式的节点会被转换为单独的图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层,比如 span 节点还是属于 div 图层。

PaintLayers(渲染层)

PaintLayer 是用来实现 stacking contest(层叠上下文),以此来保证页面元素以正确的显示顺序,这样才能正确的展示元素的重叠以及半透明元素等等

形成层叠上下文的属性条件如下:

参考 MDN 层叠上下文

GraphicsLayers(图层)

并不是所有的 PaintLayer 都能成为 GraphicsLayer,因为生成图层是会占用内存,只有某些特殊的 PaintLayer 才会被提升为 GraphicsLayer。

要能成为图层的节点除了要满足层叠上下文属性条件,还需要具有以下条件:

  • 根元素
  • 硬件加速
    • 3D transform 或 perspective 的元素
    • 硬件加速的 video
    • 3D 或硬件加速的 2D 的canvas
  • CSS3 动画:对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是激活 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
  • will-change
  • 裁剪滚动区域
  • overlaps a composited layer 的元素

以上只列举常见情况,更多详情查看

Paint:图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,但 Paint 阶段并不是真正的界面绘制,而是生成绘制指令列表,交给其他线程进行光栅任务,大大减低了主线程的负担,提高主线程效率。

可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表,如下图所示:

生成指令的过程,每一个图层都是分为多阶段并按照层叠顺序对 layout subTree 进行遍历生成指令。

下面案列 float 布局中,一个元素甚至有可能部分地位于另一个元素的前面和一部分之后,因为先绘制所有背景完后再绘制文本。

Raster:光栅化

光栅化,就是将绘制指令生成位图。

bitmap(位图)

tile

通常情况下,页面的内容都要比屏幕大得多,显示一个页面时,如果等待所有的图层都生成完毕,再进行合成的话,会产生一些不必要的开销,也会让合成图片的时间变得更久。因此,合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。不过有时候, 即使只绘制那些优先级最高的图块,也要耗费不少的时间,因为涉及到一个很关键的因素——纹理上传,这是因为从计算机内存上传到 GPU 内存的操作会比较慢。为了解决这个问题,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片。比如可以是正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。这种方式尽管会让用户在开始时看到的是低分辨率的内容,但是也比用户在开始时什么都看不到要好。

显示器显示图像原理

每个显示器都有固定的刷新频率,通常是 60HZ,也就是每秒 60 次读取显卡的前缓冲区

显卡的职责就是合成新的图像,并将图像保存到后缓冲区中,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显示器能读取到最新显卡合成的图像。

渲染引擎会通过渲染流水线生成新的图片,并发送到显卡的后缓冲区。

在 GPU 加速的场景下渲染引擎则是借助 GPU 去合成图片

渲染引擎生成的每一张图片称为一帧,每秒更新了多少帧称为帧率,比如 1 秒更新了 60 帧,那么帧率就是 60Hz(或者 60FPS)。为了保证图像显示流程,那么帧率就需要尽量达到显示器刷新频率,比如大多数设备屏幕的更新频率是 60 次 / 秒,那么渲染引擎需要每秒更新 60 张图片到显卡的后缓冲区。

现在大多数显示器刷新率在 60Hz,

不涉及图层的内容的操作能直接在合成线程中完成,比如操作的是整个图层的几何变换,透明度变换,阴影等,这些变换都不会影响到图层的内容,而文字信息的改变,布局的改变,颜色的改变,统统不会涉及,涉及到这些内容的变化就要牵涉到重排或者重绘了。

其他文章
交流区
加载中...