页面丝滑程度的优化检测

916 阅读8分钟

本文原创:fanjiayu

什么是丝滑的页面

  • chrome团队提出了一个以用户为中心的性能模型被称为RAIL,它为工程师提供一个目标,只要达到目标的网页,用户就会觉得很流畅;它将用户体验拆解为一些关键操作,例如:点击,加载等;并给这些操作规定一个目标,例如:点击一个按钮后,多长时间给反馈用户会觉得流畅。

  • RAIL将影响性能的行为划分为四个方面,分别是:response(响应)、animation(动画)、idle(空闲)与load(加载)。没错,RAIL这个名字来自于这四个单词的首字母,方便记忆。

response(响应)

  • 100ms
  • 研究表明,100ms内对用户的输入操作进行响应,通常会被人类认为是立即响应。时间再长,操作与反应之间的连接就会中断,人们就会觉得它的操作有延迟。
  • 例如:当用户点击一个按钮,如果100ms内给出响应,那么用户就会觉得响应很及时,不会察觉到丝毫延迟感。

animation(动画)

  • 60fps
  • 现如今大多数设备的屏幕刷新频率是60hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60fps,我们就会觉得动画很流畅。
  • fps指的画面每秒钟传输的帧数,**60fps指的是每秒钟60帧;**换算下来每一帧差不多是16毫秒。
  • 但通常浏览器需要花费一些时间将每一帧的内容绘制到屏幕上(包括样式计算、布局、绘制、合成等工作),所以通常我们只有10毫秒来执行JS代码。

idle(空闲)

  • 为了更好的性能,通常我们会充分利用浏览器空闲周期idle period做一些低优先级的事情。例如:在空闲周期预请求一些接下来可能会用到的数据或上报分析数据等。
  • RAIL规定,空闲周期内运行的任务不得超过50ms。因为浏览器同一时间主线程只能处理一个任务,如果一个任务执行时间过长,浏览器则无法执行其他任务,用户会感觉到浏览器被卡死了,为了达到100ms内给出响应,将空闲周期执行的任务限制为50ms意味着,即使用户的输入行为发生在空闲任务刚开始执行,浏览器仍有剩余的50ms时间用来响应用户输入,而不会产生用户可察觉的延迟

1.png

  • 事实上,不论是空闲任务还是高优先级的其他任务,执行时间都不得超过50ms。

load(加载)

  • 秒级
  • 如果不能在1秒钟内加载网页并让用户看到内容,用户的注意力就会分散。用户会觉得他要做的事情被打断,如果10秒钟还打不开网页,用户会感到失望,会放弃他们想做的事,以后他们或许都不会再回来。

丝滑页面的决定因素(像素管道)

2.png

  • JavaScript:一般来说,我们会使用JavaScript来实现一些视觉变化的效果。

  • Style:计算样式。这个过程是根据CSS选择器,对每个DOM元素匹配对应的CSS样式。

  • Layout:布局。具体计算每个DOM元素最终在屏幕上显示的大小和位置。Web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。因此,对于浏览器而言,布局过程是经常发生的。

  • Paint:绘制。本质上就是**填充像素的过程。**包括绘制文字、颜色、图像、边框和阴影等,也就是一个DOM元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。

  • Composite:渲染层合并。

3.png

  • 如果你修改一个DOM元素的“layout”属性,也就是改变了元素的样式(比如width、height或者position等),那么浏览器会检查哪些元素需要重新布局,然后对页面激发一个reflow(重排)过程完成重新布局。reflow必定会引发重绘,这对于WEB的性能影响是极大的。

  • 影响WEB性能主要过程包括layout、paint和composite。那么对于cssanimation而言,我们的所有操作都是通过CSS的样式控制动画,只要是会触发layout、paint和composite的CSS属性都会直接影响动画的性能。所以整个动画应尽量避开重排和重绘。

  • 会触发重排重绘:调整窗口大小、改变字体、增加或者移除样式表、内容变化、激活CSS伪类、计算offsetwidth和offsetheight等等。

  • 影响layout的CSS属性:csstriggers.com/

使页面丝滑的方法

####防止FSL(强制同步布局)

  • FLS:先执行JS,然后在JS中修改了样式从而导致样式计算,然后样式的改动触发了布局、绘制、合成。但JavaScript可以强制浏览器将布局提前执行,这就叫FSL强制同步布局

4.png

如下代码为会触发FSL的一段代码

<body>
    <button id="btn">Click me~</button>
    <div class="container">
        <!-- 创建多个class为box的div使效果更明显>
        <div class="box"></div>
    </div>
    <script>
        const btn = document.querySelector('#btn');
        const container = document.querySelector('.container');
        const boxes = document.querySelectorAll('.box');

        btn.onclick = function () {
            for (var i = 0; i < boxes.length; i++) {
                boxes[i].style.width = container.offsetWidth + 'px';
            }
        }
    </script>
</body>

5.png

  • 面板中已经明确给出警告:Forced reflow is a likely performance bottleneck.(“强制同步布局”可能会导致性能问题)

对代码进行如下优化

<script>
   const btn = document.querySelector('#btn');
   const container = document.querySelector('.container');
   const boxes = document.querySelectorAll('.box');

   btn.onclick = function () {
       const newWidth = container.offsetWidth;
       for (var i = 0; i < boxes.length; i++) {
           boxes[i].style.width = newWidth + 'px';
       }
   }
</script>
  • 文中前半部分已经说明,计算offsetwidth会触发重排重绘,所以第一种方法在循环中先获取容器box的宽度,随后设置了box元素的样式。这会导致浏览器去布局,然后计算样式。每次更改样式,都会导致刚刚执行的布局失效,因为我们又改了新的样式,所以下一轮循环读取宽度时,浏览器又要执行一次布局,如此反复直到循环结束。在循环期间,浏览器不停地执行无效布局,于是出现了性能问题。为了使效果更佳明显,在CPU 6x slowdown的条件下进行了测试,对比如下:

6.png
7.png

从对比中可以看出,前者的Rendering是后者的几倍之多,可见,优化重排重绘,防止FSL的发生对性能的提升效果是很明显的。

####新建图层

  • 事实上浏览器在渲染页面时,可以将页面分为很多个图层,有点类似于photoshop,一张图片在potoshop中是由多个图层组合而成,而浏览器最终显示的页面实际也是由多个图层构成的。

  • 所以将原本不断发生变化的元素提升到单独的图层中,就不再需要绘制了,浏览器只需要将两个图层合并在一起即可。(注:图层的维护也需要成本,不要一味地不断新建图层)

<body>    
    <div class="ball-running" style="width:100px;height:50px;background:red;position:absolute;left:0;top:0"></div>
</body>
<style>
    .ball-running { animation: run-around 4s infinite;} 

    @keyframes run-around {

        0%: { top: 0; left: 0; } 

        25% { top: 0; left: 200px; } 

        50% { top: 200px; left: 200px; } 

        75% { top: 200px; left: 0; } 

    }
</style>
<body>    
    <div class="ball-running" style="width:100px;height:50px;background:red;position:absolute;left:0;top:0"></div>
</body>
<style>
    .ball-running { animation: run-around 4s infinite; } 

    @keyframes run-around {

    0%: { transform: translate(0, 0); } 

    25% { transform: translate(200px, 0); } 

    50% { transform: translate(200px, 200px); } 

    75% { transform: translate(0, 200px); } 

    }
</style>
  • 二者的区别是前者不断改变元素的位置使元素运动起来,后者则通过transform来实现运动,实际上是将元素动画提升到了一个新的图层,开启GPU加速单独处理图像部分,下图是二者性能图对比。

8.png
9.png

  • 可以看到,前者不断重复发生重排和重绘过程,而后者已经不再发生重排和重绘,因为transform这个属性已经由合成器单独处理了,所以使用这个属性可以避免布局与绘制。

总结

  • JS动画要保证预留出6ms的时间给浏览器处理像素管道,而自身执行时间应该小于10ms来保证整体运行速度小于16ms。

  • 避免一切FSL,它非常影响性能。

  • CSS动画尽量使用transform属性来完成动画。建议使用transform的translate替代margin或position中的top、right、bottom和left,同时使用transform中的scalex或者scaley来替代width和height。

参考