Better

Ethan的博客,欢迎访问交流

浏览器渲染性能分析总结

作者根据谷歌开发文档总结,看后觉得一定程度上可以给日常编程中一些警示,在这里简单记录一下

概述

显示器是以一个固定速度刷新屏幕的,一般是每秒60帧,我们可以想象浏览器里面有一个保存当前浏览器内容的渲染缓存,有一个独立的线程每隔大约16.6ms从这个缓存中把浏览器内容刷新到屏幕上,而浏览器的渲染便是刷新这个缓存。

浏览器绘制一帧主要需要经过下面5步:

  1. javascript:在javascript中可以做一些引起视觉变化的动作,如修改样式、操作dom等
  2. style:这一步主要是根据选择器重新计算元素的最终的css样式,看哪些元素的样式发生了变化
  3. layout:这个阶段计算元素几何布局的变化,如位置、大小等。值的注意的是,一个元素layout的变化可能会导致其他元素的连锁变化
  4. paint:这一步就是绘制了:根据元素的位置、大小、样式进行绘制。一般来说,浏览器是分层(layer)绘制的,不同的元素可能被绘制到不同的层上
  5. composite:这一步把绘制好的层根据层级关系(如z-index)组装起来

不是每次重绘(update rendering)都会经过这完整的5步,这又分三种情况:

  • 修改layout相关属性,如width,这种情况下需要经过完整的步骤
  • 修改的属性和layout无关,如边框颜色,这种情况下不需要重新计算layout,只需要重绘
  • 有些属性的修改甚至都不需要重绘,直接组装即可

优化JS执行

避免js执行时间太长

提到js渲染优化,大家都知道一点,就是不要让js执行时间过长以免卡住主线程使得页面不能及时渲染更新,因为上面说的那几步都是在主线程中进行的。这个问题除了优化自身代码外有2种解决办法

  • web worker:比如你要做一个很费时的排序,可以扔给web worker去做,排好序了再返回
  • 任务分解:如果你的任务实在是要在主线程中做(如需要操作dom),那么可以把任务分解成很多小步,把每一小步放到requestAnimationRequest(简称raf)中进行,这样就不会阻塞页面的响应与渲染

任务分解作者有个演示代码,还挺不错的,来看看

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
​
function processTaskList(taskStartTime) {
  var taskFinishTime;
​
  do {
    // Assume the next task is pushed onto a stack.
    var nextTask = taskList.pop();
​
    // Process nextTask.
    processTask(nextTask);
​
    // Go again if there’s enough time to do the next task.
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);
​
  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);
}

requestAnimationFrame的回调函数,会传递一个类似window.performance.now()的时间戳函数。

用requestAnimationRequest来做视觉变化

简单地说,raf中注册的callback会在每一帧绘制开始的时候被调用。这里的每一帧开始可以理解为我们刚开始提到的屏幕以60帧每秒刷新的每一帧的开始,也是上一帧的结束点。就是说,从这个开始点开始,过大约16.6ms,屏幕会再次刷新。所以,你在raf中做的视觉变化(如样式修改,dom操作等)会在下一帧中得到展示(当然这些变化需要在16.6ms之内被浏览器更新)。

在raf出来之前,我们做视觉修改的时机和屏幕刷新时机是完全独立的,这会导致丢帧的情况,就是我我们的修改不会在下一帧显示出来,而是下下帧才显示出来,比如你用setTimeout在某个时间点做了修改,可能就会出现这种情形

如果我们能把js中的视觉修改提前到当前帧的开始处,那就能在下一帧得到展示,而唯一能达到这个目的的做法就是使用raf

减少样式计算的作用范围及复杂性

一是使用简单的选择器,尽量使用class

二是尽量减少需要重新计算样式的元素数量

避免布局抖动

避免复杂的布局计算以及布局的反复计算

  • 尽量避免修改元素的布局:布局计算是重新计算元素的位置及大小,由于元素之间的排版关系紧密,布局计算的范围通常是整个文档:如果文档中的元素很多,这个过程需要花很长时间,所以第一原则是尽量避免修改元素的布局
  • 避免强制布局同步(forced synchronous layouts):layout只会计算一次,但是如果我们不注意的话,可能在javascript中就会发生layout计算,这种情况叫强制布局计算,也就是通常所说的回流。关于布局,我们首先要认识的一件事就是在javascript中可以毫无代价地得到前一帧的布局信息,问题在于,如果你在获取之前改变了元素的样式,这个时候浏览器为了得到元素的最新的布局信息,必须先进行布局计算:
    function logBoxHeight() {
    // 改变元素样式
    box.classList.add('super-big');
    ​
    // Gets the height of the box in pixels
    // and logs it out - 回流产生
    console.log(box.offsetHeight);
    }
    
  • 避免布局抖动(layout thrashing):比回流更可怕的是反复回流
    function resizeAllParagraphsToMatchBlockWidth() {
    // Puts the browser into a read-write-read-write cycle.
    for (var i = 0; i < paragraphs.length; i++) {
      paragraphs[i].style.width = box.offsetWidth + 'px';
    }
    }
    

简化绘制复杂性及减小绘制区域

绘制一般是整个流程中最费时的一步,且除了transform和opacity属性外(下节会详细讲),其他css属性的修改都会引起重绘。在重绘不可避免的情况下,可以考虑以下方法来减轻重绘的代价:

  • 将重绘的元素提升到新的层
    • 浏览器是按层绘制的,绘制好所有层之后再把它们叠加合成生成最终的渲染结果。将重绘的元素提升到单独的层,这样就不会影响其他元素,提高渲染效率,这对那种移动的元素尤其有效。提升到独立的层的最有效的办法是使用will-change属性
      .moving-element {
        will-change: transform;
      }
      
    • 如果浏览器不支持这个属性,可以使用下面的规则:
      .moving-element {
        transform: translateZ(0);
      }
      
  • 减小绘制区域
  • 减小绘制复杂性:不用的css样式效果绘制效率不一样,比如说阴影绘制就比背景耗时,在效果相差不大时尽量考虑使用简单的css样式

坚持使用只影响合成的css属性

有两个属性的修改不会引起重绘,这2个属性就是 transform 和 opacity

所以在做动画时使用这2个属性是效率最高的:浏览器会把元素临时提升到独立层,不用绘制,直接合成。注意:如果需要将元素永久性地提升到独立层,需要使用上面提到的will-change或transform属性

考虑在事件处理中使用防节流机制

  • 避免在事件处理中改变样式
  • 事件节流:想scroll,size这种事件触发频率远远大于屏幕刷新频率的,在这种事件处理中做一些视觉变化操作是很浪费资源的,并可能导致界面卡死,解决办法还是一样:使用raf

来源



留言