css动画(2)-从浏览器渲染到动画性能优化

2,056 阅读8分钟

学习css动画,就得了解一下浏览器渲染原理与动画性能优化相关知识点,这样才能写出流畅的动画效果。

浏览器的进程与线程

进程与线程

  1. 进程是cpu资源分配的最小单位,进程之间相互独立,进程中有一个或多个线程
  2. 线程是cpu调度的最小单位,线程之间互相协作,共享一块内存空间

浏览器是多进程的

浏览器是多进程的,主要包括:

  • Browser进程:浏览器的主进程(负责协调、主控),只有一个
    作用有:
    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将Renderer进程得到的内存中的Bitmap,调用GPU绘制到用户界面上
    • 网络资源的管理,下载等
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU进程:最多一个,用于3D绘制等
  • 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的) 默认每打开一个tab页就有一个渲染进程,互相独立。
    主要作用为:页面渲染,脚本执行,事件处理等

注意: 浏览器有自己的优化机制,比如打开多个空白tab标签页时可能会合成为一个进程,所以一个tab页对应一个进程不是绝对的。

渲染进程

对前端而言,最重要的是渲染进程,渲染进程内部是多线程的,主要有:

  • js引擎线程

    用于执行js脚本,js是单线程机制,所有任务都得在主线程里依次运行,主线程空闲时才会处理定时器之类的任务队列。

  • GUI渲染线程

    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行,所以js会阻塞页面的渲染
  • 定时器线程
    setTimeOut和setInterval所在的线程,控制定时器的计时。定时完毕后,将定时器里的任务放入到任务队列里,等待js主线程空闲时执行

  • 异步网络请求线程
    http请求所在的线程,异步处理网络请求,如果请求有回调函数,则将回调函数放入主线程里执行。

渲染流程

  1. Brower进程接收到用户请求,首先获得网络资源,然后调用渲染进程渲染
  2. 渲染进程接到消息后简单解释一下,交给渲染线程渲染,
    • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 可能会有JS线程操作DOM(这样可能会造成回流并重绘)
    • 最后Render进程将结果传递给Browser进程
  3. Browser进程将渲染结果绘制到屏幕上

从渲染的角度来看,渲染进程也可以分为主线程和合成线程,如图所示:

主线程主要有:

  1. 使用 HTML 创建文档对象模型(DOM)
  2. 使用 CSS 创建 CSS 对象模型(CSSOM)
  3. 基于 DOM 和 CSSOM执行脚本(Scripts)
  4. 合并 DOM 和 CSSOM 形成渲染树(Render Tree)
  5. 使用渲染树布局(Layout)所有元素
  6. 渲染(Paint)所有元素 渲染进程跟gpu进程交互的过程有brower进程来控制。

层合成(composite)

首先看一张图:

  • Chrome 拥有两套不同的渲染路径:硬件加速路径和旧软件路径
  • Chrome 中有不同类型的层: RenderLayer(负责 DOM 子树)和GraphicsLayer(负责 RenderLayer的子树),只有 GraphicsLayer 是作为纹理(texture)上传给GPU的
  • 什么是纹理?可以把它想象成一个从主存储器(例如 RAM)移动到图像存储器(例如 GPU 中的 VRAM)的位图图像
  • Chrome 使用纹理来从 GPU上获得大块的页面内容。通过将纹理应用到一个非常简单的矩形网格就能很容易匹配不同的位置(position)和变形(transformation)。GPU加载位图图像比较耗时,但处理快速移动、变形则十分轻松

重绘(repaint)和重排/回流(reflow)

重绘

当我们修改页面样式时,如果没有引起布局变化,只是样式变化,如字体颜色变化,就会引起页面的重绘

重排

当页面的布局变化时,如width, margin会引起重排。
重排必定会重绘,重绘不一定重排。所以重排的开销较大。

合成层

在浏览器渲染流程中提到了composite概念,在 DOM 树中每个节点都会对应一个 LayoutObject,当他们的 LayoutObject 处于相同的坐标空间时,就会形成一个 RenderLayers ,也就是渲染层。RenderLayers 来保证页面元素以正确的顺序合成。

当页面中有不同的坐标空间时,就会存在多个渲染层,这时候就会出现层合成(composite),多个渲染层共享一个 GraphicsLayer 父层,并且正确处理透明元素和重叠元素的显示。

某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer

而每个GraphicsLayer都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后显示到屏幕上

合成层的优点

  • 合成层的位图由GPU合成,比CPU快
  • 当需要repaint时,只repaint合成层,不会影响到普通层
  • 对于transform和opacity属性,不会触发layout和paint

如何提升为合成层

  • CSS3D 属性或者 CSS 透视效果, 如transformZ(0)
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性:比如 relative etc.)(提升合成层最佳方式)
  • 对 opacity 或者 transform 使用 transition 或 animation 动画
  • 包含 video 节点对应的 RenderObject 节点
  • 包含使用 canvas 2d 或者 3d(WebGL)技术的 RenderObject 节点
  • 使用硬件加速的 CSS Filters 技术
  • 使用裁剪或者反射属性,并且其后代包含合成层
  • 有一个 Z 坐标比自己小的兄弟节点,且该兄弟节点是一个合成层

chrome控制台观察渲染效果

查看层级关系

chrome->控制台->更多工具->layers

代码:

#box1 {
    position: absolute;
    top: 50px;
    left: 0;
    width: 100px;
    height: 100px;
    background-color: pink;
}
#box2 {
    position: absolute;
    top: 200px;
    left: 0;
    width: 100px;
    height: 100px;
    background: yellowgreen;
    will-change: left; // !! 提升为合成层
}

<div id="box1"></div>
<div id="box2"></div>

可以在控制台里看到有两个层

绘制过程

chrome->控制台->更多工具->Rendering
勾选第一个和第二个,在动画过程中,repaint的部分会标为绿色,合成层则会有粉色边框。

// css
#box1 {
    width: 100px;
    height: 100px;
    background: yellowgreen;
    transition: transform 5s 0s ease-out;
}

#box2 {
    position: absolute;
    top: 200px;
    left: 0;
    width: 100px;
    height: 100px;
    background-color: pink;
    transition: left 5s 0s ease-out;
}

// html
<div id="box1"></div>
<div id="box2"></div>

// js
document.querySelector('#box1').style.transform = 'translateX(100px)'
document.querySelector('#box2').style.left = '100px'

动画过程中,控制left属性的动画,left每移动1px,就会重新计算渲染层的位图,会一直在重绘,导致页面卡顿;控制transform的动画,会自动将元素提升为合成层,合成层的重绘不会影响其他图层,同时transform属性不会引起layout和paint,动画效果明显流程很多。

如何性能优化

  • 避免在document上直接进行频繁的DOM操作,如果确实需要可以采用off-document的方式进行,具体的方法包括但不完全包括以下几种:

    (1) 先将元素从document中删除,完成修改后再把元素放回原来的位置
    (2) 将元素的display设置为”none”,完成修改后再把display为原来的值
    (3) 如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document

  • 集中修改样式

    (1) 尽可能少的修改元素style上的属性

    (2) 尽量通过修改className来修改样式

    (3) 通过cssText属性来设置样式

  • 缓存Layout属性值

    浏览器底层会对样式修改做一些优化,浏览器不会逐条修改样式,而是维护了一个渲染任务队列,浏览器会根据具体的需要分批集中执行其中的任务。除了浏览器自身维护的定期调度之外,脚本中的某些操作会导致浏览器立即执行渲染任务,例如读取元素的Layout属性,如offsetTop, scrollTop等;

    对于Layout属性中非引用类型的值(数字型),如果需要多次访问则可以在一次访问时先存储到局部变量中,之后都使用局部变量,这样可以避免每次读取属性时造成浏览器的渲染。

  • 使用transform或者opacity做动画
    因为transform和opacity做动画可以提升为合成层,同时不会引起layout和paint,所以尽量使用这两个属性做动画,如果不能实现的效果则可以强制将元素提升为合成层,使用gpu加速。