浏览器渲染机制

783 阅读12分钟

1. 浏览器的渲染过程

常见的渲染引擎

  • Webkit:Safari
  • Blink(Webkit fork):Chromium/Chrome\Opera、Microsoft Edge
  • Gecko:FireFox
  • Trident:IE、Edge(旧)

渲染引擎: 能够将HTML/CSS/JavaScript文本及资源文件转换成图像结果

Chrome架构

多进程浏览器

  • Browser:地址栏目、书签、网络请求、文件访问等
  • Renderer:负责显示网站的选项卡内所有内容
  • Plugin:控制网站使用的所有插件
  • GPU:独立于其他进程的GPU处理任务,只有一个GPU进程(处理来自多个程序的请求并将它们绘制在一个页面)

渲染器进程

  • 渲染器进程负责选项卡内发生的所有事情,在渲染进程中,主线程处理大部分代码
  • Webwork 或 service worker会开启另一个线程(用来解决任务队列堵塞问题

渲染过程:解析部分

  • 构建DOM树
  • 子资源加载(JavaScript可以组织解析)
  • 提示浏览器如何加载资源
  • 样式表计算
  • 布局——Layout
  • 绘制——Paint

渲染过程:合成部分

  • 把文档结构,元素样式,几何形状和绘制顺序转换成屏幕的像素(简称:光栅化

  • 合成是将页面的各个部分分层,分别栅格化,最后在一个合成器线程中合称为页面

    • 获取 DOM 并将其分割为多个层
    • 将每个层独立的绘制进行位图中
    • 将层作为纹理上传至 GPU(显卡)
    • 复合多个层来生成最终的屏幕图像

渲染过程:GPU渲染

  • 创建dom树确定绘制顺序(Layer)后,主线程将渲染信息交给合成器线程,合成器将图层栅格化,并且将图层分解成图块,栅格线程栅格化每个图块并存储在GPU中
  • 合成器进程通过IPC(组件间通信)把合成器帧发送到浏览器进程;这时调用UI进程使用合成器帧对浏览器UI更改,然后合成器帧发送到GPU用来屏幕显示。(屏幕发生滑动会创建另外一个合成器帧发送到GPU)
  • 合成是在GPU线程下完成的,不涉及主线程,因此在DOM树确立后,不再更改布局和绘图,使用合成动画能够提高性能。

分层机制: 一个网页是由多个层展示的,然后再将这些层合并层一个层,当dom或样式变化时,GPU(显卡)能够缓存一些不变的内容(层),将要变化的层与缓存层再合成,提高渲染效率,因此在做动画时让GPU参与进来,提高动画性能

Layer模型:

  • 浏览器根据CSS属性为元素生成Layers
  • 将Layers作为位图上传到GPU
  • 当改变Layer的transform,opcity等属性时,渲染会跳过Layout,paint,直接通知GPU对Layer做变换

深入理解重排和重绘

重排: 当DOM的变化影响了元素的几何属性(宽或高),浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排。

重绘: 完成重排后,重新绘制受影响的部分到屏幕,该过程称为重绘。

不管是重排还是重绘都会都会让主线程参与到画面的渲染(Layer)中,影响到浏览器的性能。

需要尽量减少重排,重绘。

  1. 样式表越简单,重排、重绘越快
  2. 重排、重绘的DOM层级越高,渲染成本越高
  3. table元素的重排重绘成本,高于div元素
  4. 读写操作尽量分离
  5. 样式采用主题形式,统一改变
  6. 缓存重排结果
  7. 离线DOM Fragment、虚拟DOM
  8. 不可见元素不影响重排重绘(display: none)

2. 理解解析渲染和代码优化

想要提高网页加载速度,提升用户体验,就需要在第一次加载时让重要的元素尽快显示在屏幕上。而不是等所有元素全部准备就绪再显示,下面一幅图说明了这两种方式的差异。

image.png

Critical Rendering Path

关键渲染路径: 指的是浏览器从请求HTML、CSS、JavaScript文件到渲染到屏幕的这一过程。

  1. 构建DOM树:这个过程是循序渐进的,我们假设HTML文件很大,一个RTT (Round-Trip Time,往返时延) 只能得到一部分,浏览器得到这部分之后就会开始构建DOM,并不会等到整个文档就位才开始渲染

    1. 将HTML解析成许多Tokens
    2. 将Tokens解析成object
    3. 将object组合成DOM树
  2. 构建CSSOM:解析过程可以和构建DOM同时进行,但是CSSOM则必须等到所有字节收到才开始构建。

    • 解析CSS文件,并构建出一个CSSOM树
  3. 构建Render tree:浏览器使用DOM和CSSOM构建出Render Tree。此时不像构建DOM一样把所有节点构建出来,浏览器只构建需要在屏幕上显示的部分。(head,meta等标签不会显示)——对应Dev Tools中的Layout过程

    • 结合DOM和CSSOM构建
  4. Layout:计算元素的位置和大小

  5. Paint:将render tree转换成像素,显示到屏幕。对应Dev Tool Performance Panel里Paint过程

注意: 并不是依次进行的,会存在一定的交叉

引入JavaScript

因为JavaScript会影响关键路径的渲染队列;script标签尽量放在最后面

  1. 解析HTML构建DOM时,遇到JavaScript会被阻塞

  2. JavaScript执行会被CSSOM构建阻塞,也就是说,JavaScript必须等到CSSOM构建完成后才会执行

  3. 如果使用异步脚本,脚本的网络请求优先级降低,且网络请求期间不阻塞DOM构建,直到请求完成才开始执行脚本。

    异步脚本<script src="" async />当网络请求完成时触发,在下载完毕后执行。

    异步脚本<script src="" defer />页面加载结束触发JavaScript,Document被解析完毕而DOMContentLoaded事件触发之前执行。

浏览器合成层与页面优化

 filter: blur(100px); 

这行 CSS 代码用于实现一个高斯模糊,当存在多个元素拥有该属性时,浏览器渲染可能会十分吃力;导致渲染进程的CPU占用率过高,导致卡顿。

 will-change: transform; 

原理其实很简单,这行代码能够开启 GPU 加速页面渲染,从而大大降低了 CPU 的负载压力,达到优化页面渲染性能的目的。

浏览器的渲染流程

除去网络资源获取的步骤,我们理解的 Web 页面的展示,一般可以分为 构建 DOM 树、构建渲染树、布局、绘制、渲染层合成 几个步骤。

  1. 构建DOM树:浏览器将HTML解析成树状结构的DOM树,发生在页面初次加载或修改了元素节点的结构的时候。
  2. 构建渲染树:浏览器将CSS解析成树状结构的CSSOM树,再和DOM树合并成渲染树。
  3. 布局(Layout):浏览器根据渲染树的节点以及对应的CSS样式,计算出各节点在屏幕上的位置。当元素的位置、大小发生变化时,会导致其他节点起连锁反应重新布局,这个布局的过程成为回流(Reflow)
  4. 绘制(Paint):遍历渲染树,调用渲染器的方法(paint())绘制出节点内容——像素填充。这个过程出现于回流或其他不影响布局的CSS变化引起的局部重画,这称为重绘(Repaint)
  5. 渲染层合成(Composite):多个绘制后的渲染层按照CSS的层级样式进行合并,生成位图,最终通过显卡展示到屏幕上。

浏览器的渲染原理

根据上述的渲染流程,DOM树到屏幕显示的转化原理,本质上是树结构到层结构的演化。

image.png

  1. 渲染对象——RenderObject

    一个节点对应一个渲染对象,渲染对象们组合起来维持DOM树的树形结构;渲染对象通过一个绘图上下文(GraphicsContext)调用对应的方法绘制DOM节点

  2. 渲染层——RenderLayer

    渲染期间的第一个层模型,在同一个Z轴坐标的渲染对象会被渲染在同一个渲染层,根据不同的层叠上下文,形成不同的渲染层表示他们的层级关系。

    对于满足形成层叠上下文条件的渲染对象,浏览器会自动为其创建新的渲染层。能够导致浏览器为其创建新的渲染层的,包括以下几类常见的情况:

    • 根元素 document
    • 有明确的定位属性(relative、fixed、sticky、absolute)
    • opacity < 1
    • 有 CSS fliter 属性
    • 有 CSS mask 属性
    • 有 CSS mix-blend-mode 属性且值不为 normal
    • 有 CSS transform 属性且值不为 none
    • backface-visibility 属性为 hidden
    • 有 CSS reflection 属性
    • 有 CSS column-count 属性且值不为 auto或者有 CSS column-width 属性且值不为 auto
    • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画

    • overflow 不为 visible

      不满足上述条件的将会与其第一个拥有渲染层的父元素共用同一个渲染层,因此实际上,这些渲染对象会与它的部分子元素共用这个渲染层。

  3. 图形层——GraphicsLayer

    一个重要的渲染载体和工具,但它并不直接处理渲染层,而是处理合成层。

    一个负责生成最终准备呈现的内容图形的层模型,它拥有一个图形上下文,负责输出该层的位图。存储在共享内存中的位图将作为纹理上传到 GPU,最后由 GPU 将多个位图进行合成,然后绘制到屏幕上。

  4. 合成层——CompositingLayer

    渲染层满足对应条件会提升至合成层;合成层拥有独立的图形层,其他不是合成层的会和父级共用一个。

    常见提成至合成层的情况:

    • 3D transforms:translate3d、translateZ 等
    • video、canvas、iframe 等元素
    • 通过 Element.animate() 实现的 opacity 动画转换
    • 通过 СSS 动画实现的 opacity 动画转换
    • position: fixed
    • 具有 will-change 属性
    • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition

    将 CPU 消耗高的渲染元素提升为一个新的合成层,才能开启 GPU 加速的

    隐式合成

    一个或多个非合成元素应出现在堆叠顺序上的合成元素之上,被提升到合成层。

    • 绝对定位的两个元素,由于z-index不同,会产生覆盖的情况
    • 使用transform: translateZ()修改元素的Z轴,使其在和同级的元素产生了交叠关系,这时会将该被修改的元素提升到合成层

    层爆炸:

    过多的层级会占用 GPU 和大量的内存资源,严重损耗页面性能,因此盲目地使用 GPU 加速,结果有可能会是适得其反。

    隐式合成在不需要交叠的情况下也能发生,消除隐式合成就是要消除元素交叠。

    层压缩:

    多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”。

    浏览器如何查看合成层——Chrome Devtools

image-20211114205720753.png

优化建议

1.动画使用transform实现

这样做的原因是:

  1. 如果使用 left/top 来实现位置变化,animation 节点和 Document 将被放到了同一个 GraphicsLayer 中进行渲染,持续的动画效果将导致整个 Document 不断地执行重绘
  2. 而使用 transform 的话,能够让 animation 节点被放置到一个独立合成层中进行渲染绘制,动画发生时不会影响到其它层。
  3. 动画会完全运行在 GPU 上,相比起 CPU 处理图层后再发送给显卡进行显示绘制来说,这样的动画往往更加流畅

2.减少隐式合成

从根本上来说是为了保证正确的图层重叠顺序,但具体到实际开发中,隐式合成很容易就导致一些无意义的合成层生成,归根结底其实就要求我们在开发时约束自己的布局习惯,避免踩坑。

并不是盲目地设置 z-index 就能避免,有时候 z-index 也还是会导致隐式合成,这个时候可以试着调整一下文档中节点的先后顺序直接让后边的节点来覆盖前边的节点。

方法不是唯一的,具体方式还是得根据不同的页面具体分析。

3. 减小合成层的尺寸

.bottom {
    width: 100px;
    height: 100px;
    top: 20px;
    left: 20px;
    z-index: 3;
    background: rosybrown;
  }
  .top {
    width: 10px;
    height: 10px;
    transform: scale(10);
    top: 200px;
    left: 200px;
    z-index: 5;
    background: indianred;
  }

利用 Chrome Devtools 查看这两个合成层的内存占用后发现,.bottom 内存占用是 39.1 KB,而 .top 是 400 B,差距十分明显。

对于一些纯色图层来说,我们可以使用 width 和 height 属性减小合成层的物理尺寸,然后再用 transform: scale(…) 放大,这样一来可以极大地减少层合成带来的内存消耗。

冷知识

Window: beforeunload event

当浏览器窗口关闭或者刷新时,会触发beforeunload事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。

BubblesNo
CancelableYes
InterfaceEvent
Event handler propertyonbeforeunload

事件使网页能够触发一个确认对话框,询问用户是否真的要离开该页面。如果用户确认,浏览器将导航到新页面,否则导航将会取消。

根据规范,要显示确认对话框,事件处理程序需要在事件上调用preventDefault()

但是请注意,并非所有浏览器都支持此方法,而有些浏览器需要事件处理程序实现两个遗留方法中的一个作为代替:

  • 将字符串分配给事件的returnValue属性
  • 从事件处理程序返回一个字符串。
window.addEventListener('beforeunload', (event) => {
  // Cancel the event as stated by the standard.
  event.preventDefault();
  // Chrome requires returnValue to be set.
  event.returnValue = '';
});