网络知识 娱乐 渲染优化小结

渲染优化小结

渲染优化

  • 浏览器从获取HTML到渲染到屏幕上主要需要以下几步
  1. 处理 HTML 标记并构建 DOM 树
  2. 处理 CSS 标记并构建 CSSOM 树
  3. 将 DOM 和 CSSOM 合并成一个 render tree
  4. 根据渲染树来布局,以计算每隔节点的几何信息
  5. 将各个节点绘制到屏幕上
  • 经过以上整个流程我们才能看见屏幕上出现渲染的内容,优化关键渲染路径就是指最大限度缩短执行上述第 1 步至第 5 步耗费的总时间,让用户最快的看到首次渲染的内容。

  • 不但网站页面要快速加载出来,而且运行过程也应更顺畅,在响应用户操作时也要更加及时,比如我们通常使用手机浏览网上商城时,指尖滑动屏幕与页面滚动应很流畅,拒绝卡顿。那么要达到怎样的性能指标,才能满足用户流畅的使用体验呢?

  • 目前大部分设备的屏幕分辨率都在60fps左右,也就是每秒屏幕会刷新60次,所以要满足用户的体验期望,就需要浏览器在渲染页面动画或响应用户操作时,每一帧的生成速率尽量接近屏幕的刷新率。若按照60fps来算,则留给每一帧画面的时间不到17ms,再除去浏览器对资源的一些整理工作,一帧画面的渲染应尽量在10ms内完成,如果达不到要求而导致帧率下降,则屏幕上的内容会发生抖动或卡顿。

  • 为了使每一帧页面渲染的开销都能在期望的时间范围内完成,就需要开发者了解渲染过程的每个阶段,以及各阶段中有哪些优化空间是我们力所能及的。经过分析根据开发者对优化渲染过程的控制力度,可以大体将其划分为五个部分:JavaScript处理、计算样式、页面布局、绘制与合成,下面先简要介绍各部分的功能与作用。
    在这里插入图片描述

  1. JavaScript 处理:前端项目中经常会需要响应用户操作,通过 JavaScript 对数据集进行计算、操作 DOM 元素,并展示动画等视觉效果。当然对于动画的实现,除了 JavaScript,也可以考虑使用如 CSS Animations、Transitions 等技术。
  2. 计算样式:在解析 CSS 文件后,浏览器需要根据各种选择器去匹配所要应用 CSS 规则的元素节点,然后计算出每个元素的最终样式。
  3. 页面布局:指的是浏览器在计算完成样式后,会对每个元素尺寸大小和屏幕位置进行计算。由于每个元素都可能会受到其他元素的影响,并且位于 DOM 树形结构中的子节点元素,总会受到父级元素修改的影响,所以页面布局的计算会经常发生。
  4. 绘制:在页面布局确定后,接下来便可以绘制元素的可视内容,包括颜色、边框、阴影及文本和图像。
  5. 合成:通常由于页面中的不同部分可能被绘制在多个图层上,所以在绘制完成后需要将多个图层按照正确的顺序在屏幕上合成,以便最终正确地渲染出来。
  • 这个过程中的每一阶段都有可能产生卡顿,本章后续内容将会对各阶段所涉及的性能优化进行详细介绍。这里值得说明的是,并非对于每一帧画面都会经历这五个部分。比如仅修改与绘制相关的属性(文字颜色、背景图片或边缘阴影等),而未对页面布局产生任何修改,那么在计算样式阶段完成后,便会跳过页面布局直接执行绘制。

关键路径渲染优化

  • 为了尽快完成首次渲染,我们需要最大限度减少一下三种可变因素
  1. 关键资源数量(影响首次渲染的资源数量要少, 如 js css)
  2. 关键路径长度(一方面某些资源只有在别的资源加载完成之后才能加载,另一方面资源越大下载时间会很长,我们应尽量减少依赖或减小体积 这就是减小它的关键路径长度)
  3. 关键字节数量(我们应该要减少字节数,我们可以减少资源数(将他们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减少传送大小。)
  • 优化DOM
    • 总结起来有三种方式可以优化 HTML : 缩小文件尺寸 (Minify),使用 gzip 压缩(Compress), 使用缓存(HTTP Cache)

    • 缩小文件尺寸(Minify)会删除注释,空格,与换行等无用的文本。

    • 本质上,优化 DOM 其实就是尽可能的减少关键路径的长度与关键字节的数量

  • 优化CSSOM
    • 确保将任何非必需的 CSS 都标记为非关键资源(例如打印和其他媒体查询),并应确保尽可能减少关键 CSS 的数量,以及尽可能手段传送时间。分文件加载,首先加载通用的css然后加载用于打印或者在其他条件下才会渲染的css样式文件
    • 避免在css中再@import另外的css,这样会使关键路径长度变长
  • 优化js的使用
    • 删除未使用的代码,缩小文件尺寸 (Minify),使用 gzip 压缩(Compress), 使用缓存(HTTP Cache)
    • 异步加载js,避免js中同步请求, 延迟解析js
      • 在script标签上添加defer,async都可以异步加载js,不会阻塞dom的渲染,defer在异步加载js的同时还能保持相对引用顺序,例如第二行的js依赖第一行的js内的函数,那么虽然它们都是异步加载并行在后台下载,第二行引用的js文件就算先下载完成也会等第一行引用的js文件下载完了之后再运行,但是async标示就不会这样了,async标示更适合给第三方js库使用
    • 避免运行时间长的js可以预加载资源
       
       <!-- 
        -->
      
       
       
       
      

    js执行优化

    • 实现动画效果优化:js实现动画效果使用setInterval的时候因为会被放在异步队列,只有当主线程任务执行完成之后才会执行,所以会与设定执行的时间之间存在误差,且设备的刷新频率不同,定时器设定的刷新时间不一定与所有设备的刷新频率同步,所以会造成丢帧现象,这时可以使用window.requestAnimationFram方法,此方法与setInterval相比,最大好处是刷新频率由系统决定,可顺应设备的刷新频率,但是使用的时候得考虑好兼容问题。
    • 恰当的使用web worker
    • 使用节流和防抖

计算样式优化

  • 尽量避免使用 .list-page li {} 这样的形式来定义css样式,因为浏览器是从右到左计算的,会先遍历li标签 然后再找li标签的父元素为 .list-page的元素, 建议都使用类名形式
  • 尽量避免使用通配符 * {} ,通配符里面定义的css样式浏览器需要去遍历每个元素
  • 避免使用复杂选择器 如 .container:nth-last-child(-n+1) .content
  • 使用BEM规范
    • BEM 是一种 CSS 的书写规范,它的名称是由三个单词的首字母组成的,分别是块(Block)、元素(Element)和修饰符(Modifier)。理论上它希望每行 CSS 代码只有一个选择器,这就是为了降低选择器的复杂性,对选择器的命名要求通过以下三个符号的组合来实现。
      • 中画线(-):仅作为连字符使用,表示某个块或子元素的多个单词之间的连接符。
      • 单下画线(_):作为描述一个块或其子元素的一种状态。
      • 双下画线(__):作为连接块与块的子元素。
      • 例如 type-block__element_modifier
      • 通常来说,凡是独立的页面元素,无论简单或是复杂都可以被视作一个块,在 HTML 文档中会用一个唯一的类名来表示这个块。具体的命名规则包括三个:只能使用类选择器,而不使用ID选择器;每个块应定义一个前缀用来表示命名空间;每条样式规则必须属于一个块。比如一个自定义列表就可视作为一个块,其类名匹配规则可写为:.mylist {}
    • 元素
      // 常规写法
        .mylist {}
        
        .mylist .item {}
        
        // BEM 写法
        .mylist {}
        .mylist__item {}
      
    • 修饰符
      • 修饰符可以看作是块或元素的某个特定状态,以按钮为例,它可能包含大、中、小三种默认尺寸及自定义尺寸,对此可使用 small,
        normal , big , 或者 size-N 来修饰具体按钮的选择器类名,示例如下:
        /* 自定义列表下子元素大、中、小三种尺寸的类选择器 */
        .mylist__item_big {}
        
        .mylist__item_normal {}
        
        .mylist__item_small {}
        
        /* 带自定义尺寸修饰符的类选择器 */
        .mylist__item_size-10 {}
        
  • BEM 样式编码规范建议所有元素都被单一的类选择器修饰,从 CSS 代码结构角度来说这样不但更加清晰,而且由于样式查找得到了简化,渲染阶段的样式计算性能也会得到提升。

页面布局与重绘优化

  • 页面布局也叫作重排和回流,指的是浏览器对页面元素的几何属性进行计算并将最终结果绘制出来的过程。凡是元素的宽高尺寸、在页面中的位置及隐藏或显示等信息发生改变时,都会触发页面的重新布局。
  • 通常页面布局的作用范围会涉及整个文档,所以这个环节会带来大量的性能开销,我们在开发过程中,应当从代码层面出发,尽量避免页面布局或最小化其处理次数。如果仅修改了 DOM 元素的样式,而未影响其几何属性时,则浏览器会跳过页面布局的计算环节,直接进入重绘阶段。
  • 虽然重绘的性能开销不及页面布局高,但为了更高的性能体验,也应当降低重绘发生的频率和复杂度。本节接下来便针对这两个环节的性能优化给出一些实用性的建议。
  • 触发回流与重绘的操作大概有以下三种
    • 首先就是对 DOM 元素几何属性的修改,这些属性包括 width ,height,padding,margin,left, top 等,某元素的这些属性发生变化时,便会波及与它相关的所有节点元素进行几何属性的重新计算,这会带来巨大的计算量;
    • 其次是更改 DOM 树的结构,浏览器进行页面布局时的计算顺序,可类比树的前序遍历,即从上向下、从左向右。这里对 DOM 树节点的增、删、移动等操作,只会影响当前节点后的所有节点元素,而不会再次影响前面已经遍历过的元素;
    • 最后一类是获取某些特定的属性值操作,比如页面可见区域宽高 offsetWidth, offsetHeight,页面视窗中元素与视窗边界的距离 offsetTop ,offsetLeft, ,页面视窗中元素与视窗边界的距离 srcollTop, srcollLeft, srcollRight, clientTop, clientWidth, clientHeight 以及调用window.getComputedStyle 方法。
    • 这些属性和方法有一个共性,就是需要通过即时计算得到,所以浏览器就需要重新进行页面布局计算。
  • 避免样式频繁改动
    • 在通常情况下,页面的一帧内容被渲染到屏幕上会按照如下顺序依次进行,首先执行JavaScript代码,然后依次是样式计算、页面布局、绘制与合成。如果在JavaScript运行阶段涉及上述三类操作,浏览器就会强制提前页面布局的执行,为了尽量降低页面布局计算带来的性能损耗,我们应当避免使用JavaScript对样式进行频繁的修改。如果一定要修改样式,则可通过以下几种方式来降低触发重排或回流的频次。
      • 使用类名对样式逐条修改
      • 缓存对敏感属性值的计算:例如js计算高度宽度之后再统一赋值触发回流,不要计算完高度赋值一次,然后计算完宽度又赋值一次
      • 使用 requestAnimationFrame 方法控制渲染帧
      • 通过chrome工具监控渲染情况,可以在“设置”→“更多工具”中,发现许多很实用的性能辅助小工具,比如监控渲染的 Rendering 工具,打开 Rendering 的工具面板后,会发现许多功能开关与选择器,下面举例介绍其中若干常用功能项。
        首先是 Paint flashing,当我们开启该功能后,操作页面发生重新渲染,Chrome 会让重绘区域进行一次绿色闪动。
        这样就可以通过观察闪动区域来判断是否存在多余的绘制开销,比如若仅单击 Select 组件弹出下拉列表框,却发现整个屏幕区域都发生了闪动,或与此操作组件的无关区域发生了闪动,这都意味着有多余的绘制开销存在,需要进一步研究和优化。
        在这里插入图片描述
    • 降低绘制复杂度,例如阴影的绘制是否可以做成背景图来实现,而不用css实现

    合成处理

    • 合成处理是将已绘制的不同图层放在一起,最终在屏幕上渲染出来的过程。在这个环节中,有两个因素可能会影响页面性能:一个是所需合成的图层数量,另一个是实现动画的相关属性。
    • 新增图层
      • 在降低绘制复杂度小节中讲到,可通过将固定区域和动画区域拆分到不同图层上进行绘制,来达到绘制区域最小化的目的。接下来我们就来探讨如何创建新的图层,最佳方式便是使用 CSS 属性 will-change 来创建
         .nav-layer {
         	will-change: transform;
         }
      
      该方法在 Chrome、Firefox 及 Opera 上均有效,而对于 Safari 等不支持 will-change 属性的浏览器,则可以使用 3D 变换来强制创建:
         .new-layer {
         	transform: translate(0);
         }
      
      • 虽然创建新的图层能够在一定程度上减少绘制区域,但也应当注意不能创建太多的图层,因为每个图层都需要浏览器为其分配内存及管理开销。如果已经将一个元素提升到所创建的新图层上,也最好使用 Chrome 开发者工具中的 Layers 对图层详情进行评估,确定是否真的带来了性能提升,切忌在未经分析评估前就盲目地进行图层创建
    • 仅与合成相关的动画属性
      • 在了解了渲染过程各部分的功能和作用后,我们知道如果一个动画的实现不经过页面布局和重绘环节,仅在合成处理阶段就能完成,则将会节省大量的性能开销。目前能够符合这一要求的动画属性只有两个:透明度 opaccity 和图层变换 transform 。它们所能实现的动画效果如表所示,其中用 n 来表示数字。
        在这里插入图片描述
        在使用opacity 和 transform 实现相应的动画效果时,需要注意动画元素应当位于独立的绘图层上,以避免影响其他绘制区域。这就需要将动画元素提升至一个新的绘图层。