威尼斯使命未经作者许可。

高性能移动端开发

2017/02/20 · JavaScript
· 浏览器,
渲染

本文作者: 伯乐在线 –
陈被单
。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

众所周知,网页不仅应该被快速加载,同时还应该流畅运行,比如快速响应的交互,如丝般顺滑的动画……

在实际开发中如何做到上面所说的效果呢?

    1. 确认渲染性能的分析标准
    1. 准备尺子去衡量渲染性能标准
    1. 对耗时多的地方进行优化

我们可以粗略的得到下面的优化目标

威尼斯使命 1

第一个是
首屏呈现时间,网上的资料已经非常非常多了,压缩代码,使用webp图片,使用sprite,按需加载,“直出”,CDN……

第二个是 16ms 优化,本篇重点讲16ms的优化。

一. 浏览器渲染原理介绍

大多数设备的刷新频率是60次/秒,(1000/60 =
16.6ms)也就说是浏览器对每一帧画面的渲染工作要在16ms内完成,超出这个时间,页面的渲染就会出现卡顿现象
,影响用户体验。

这就是上图中的

威尼斯使命 2

如果改变属性在上面图中越往左,那么影响就越大,效率就越低。

浏览器渲染的流程如下:

  1. 获取 DOM 并将其分割为多个层(RenderLayer)
  2. 将每个层栅格化,并独立的绘制进位图中
  3. 将这些位图作为纹理上传至 GPU
  4. 复合多个层来生成最终的屏幕图像(终极layer)。

从上面图中可以看出,如果只是改变composite(渲染层合并),那效率就会大大提高。

下面粗略地列出改变哪些样式会分别改变渲染过程的哪一模块。

威尼斯使命 3

从上图可以看到 transform,opacity
只会改变composite(渲染层合并),为什么呢?因为开启了GPU加速。

开启 GPU 加速

字面上的解释:
纹理能够以很低的代价映射到不同的位置,而且还能够以很低的代价通过把它们应用到一个非常简单的矩形网格中进行变形。

【字面上的理解非常地绕口,还是老道理,能用图讲清的道理不要用文字。】

小tips:先选中timeline的某一帧,然后选择下面的layer标签tab,可以左右拖动该模块出现3d

我们可以看到页面上由如下层组成:

威尼斯使命 4

虽然我们最终在浏览器上看到的只是一个复印版,即最终只有一个层。类似于PhotoShop软件中的“图层”概念,最后合并所有可视图层,输出一张图片到屏幕上

但是实际上一个页面会因为一些规则被分成相应的层,一旦被独立出来之后,便不会再影响其他dom的布局,因为它改变之后,只是“贴上”了页面。
目前下面这些因素都会引起Chrome创建层:

  • 3D 或透视变换(perspective transform) CSS 属性
  • 使用加速视频解码的 <video> 元素
  • 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 <canvas> 元素
  • 混合插件(如 Flash)
  • 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index
    较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
  • 在webkit内核的浏览器中,如果有上述情况,则会创建一个独立的layer。

需要注意的是,不要创建过多的渲染层,这意味着新的内存分配和更复杂的层管理。不要滥用GPU加速,注意看
composite layouts 是否超出了 16ms

威尼斯使命 5

说了这么多浏览器渲染的原理,如果没有尺子测量也毫无用处。那么,下面就选尺子去丈量:谷歌开发工具的Timeline。

二. 谷歌开发工具 Timeline 的常用功能

1.
点击左上角的录制之后,录制结束后会生成下面的样子,红色区域内就是帧了,移动上去可以看到每一帧的频率,如果>60fps,就是比较流畅,如果

威尼斯使命 6

威尼斯使命 7

2.
在timeline下面,可以看到各个模块的耗时,可以定位到耗时较大的函数上面,对该函数进行优化。

威尼斯使命 8

3.
按照下面步骤选择,即可看到独立的层,高亮重绘的区域,方便找出不必要重绘的区域,进行优化

威尼斯使命 9

威尼斯使命 10
选择之后,当前页面会出现下面2中颜色边框

黄色边框: 有动画3d变换的元素,表示放到了一个新的复合层(composited
layer)中渲染

蓝色的栅格:这些分块可以看作是比层更低一级的单位,Chrome以这些分块为单位,一次向GPU上传一个分块的内容。

工具也有了,浏览器渲染的原理也知道了,接下来是结合实际项目进行优化.

三. 在实际项目中进行 16.6ms 优化

结合上面的渲染流程图,我们可以针对性的分析并优化下面的一些步骤

  • 优化JavaScript的执行效率
  • 降低样式计算的范围和复杂度
  • 避免大规模、复杂的布局
  • 简化绘制的复杂度、减少绘制区域
  • 优先使用渲染层合并属性、控制层数量
  • 对用户输入事件的处理函数去抖动(移动设备)

1. 读写分离,批量操作 

JavaScript脚本运行的时候,它能获取到的元素样式属性值都是上一帧画面的,都是旧的值。

因此,如果你在当前帧获取属性之前又对元素节点有改动,那就会导致浏览器必须先应用属性修改,结果执行布局过程,最后再执行JavaScript逻辑。

// 先写后读,触发强制布局 function logBoxHeight() { // 更新box样式
box.classList.add(‘super-big’); // 为了返回box的offersetHeight值 //
浏览器必须先应用属性修改,接着执行布局过程
console.log(box.offsetHeight); }

1
2
3
4
5
6
7
8
9
// 先写后读,触发强制布局
function logBoxHeight() {
    // 更新box样式
    box.classList.add(‘super-big’);
 
    // 为了返回box的offersetHeight值
    // 浏览器必须先应用属性修改,接着执行布局过程
    console.log(box.offsetHeight);
}

优化之后:

// 先读后写,避免强制布局 function logBoxHeight() { //
获取box.offsetHeight console.log(box.offsetHeight); // 更新box样式
box.classList.add(‘super-big’); }

1
2
3
4
5
6
7
8
// 先读后写,避免强制布局
function logBoxHeight() {
    // 获取box.offsetHeight
    console.log(box.offsetHeight);
 
    // 更新box样式
    box.classList.add(‘super-big’);
}

2. 闭包缓存计算结果   (需要频繁的调用,计算的函数)

1 getMaxWidth: (function () { 2 var cache = {}; 3 function getwidth() {
4 if (maxWidth in cache) { 5 return cache[maxWidth]; 6 } 7 var target
= this.node, 8 width = this.width, 9 screen = document.body.clientWidth,
10 num = target.length, 11 maxWidth = num * width + 10 * num + 20 –
screen; 12 cache[maxWidth] = maxWidth; 13 return maxWidth; 14 } 15
return getwidth; 16 })(),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1 getMaxWidth: (function () {
2             var cache = {};
3             function getwidth() {
4                 if (maxWidth in cache) {
5                     return cache[maxWidth];
6                 }
7                 var target = this.node,
8                     width = this.width,
9                     screen = document.body.clientWidth,
10                     num = target.length,
11                     maxWidth = num * width + 10 * num + 20 – screen;
12                 cache[maxWidth] = maxWidth;
13                 return maxWidth;
14             }
15             return getwidth;
16 })(),

改成这种方式后,直接蹭蹭蹭~ 减少了10多ms

3. 对用户输入事件的处理函数去抖动

如果被触摸的元素绑定了输入事件处理函数,比如touchstart/touchmove/touchend,那么渲染层合并线程必须等待这些被绑定的处理函数执行完毕才能执行,也就是用户的滚动页面操作被阻塞了,表现出的行为就是滚动出现延迟或者卡顿。

简而言之就是你必须确保用户输入事件绑定的任何处理函数都能够快速的执行完毕,以便腾出时间来让渲染层合并线程完成他的工作。

输入事件处理函数,比如scroll/touch事件的处理,都会在requestAnimationFrame之前被调用执行。因此,如果你在上述输入事件的处理函数中做了修改样式属性的操作,那么这些操作就会被浏览器暂存起来。

然后在调用requestAnimationFrame的时候,如果你在一开始就做了读取样式属性的操作,那么将会触发浏览器的强制同步布局操作(即在javascript阶段中执行布局),这样会导致多次布局,效率低下。

优化如下:

window.requestAnimationFrame(function () { context.animateTo(nowPos);
//需要更新位置的交给RAF });

1
2
3
window.requestAnimationFrame(function () {
    context.animateTo(nowPos);  //需要更新位置的交给RAF
});

4. 减少不必要的重绘

续上面,开启paint flashing
之后,可以看到浏览器重新绘制了哪些区域。发现有一些不必要重绘的区域也重绘了~给这些开启GPU优化(上文中提到)

直接看 timeline 效果,全绿了~悬着的心终于放下了

威尼斯使命 11

威尼斯使命 12

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

威尼斯使命 13
威尼斯使命 14

1 赞 6 收藏
评论

关于作者:陈被单

威尼斯使命 15

热爱前端,欢迎交流
个人主页 ·
我的文章 ·
19 ·
  

威尼斯使命 16

发表评论

电子邮件地址不会被公开。 必填项已用*标注