前端交互动画优化

原文连接:bluest.mecss

 

前端优化是个很普遍的命题,铺开去得出本书了(事实上我也没那本事),实际上市面上也有不少相关的书籍。动画与交互上的性能问题最容易被察觉,特别是在机能较低的移动端。因为本身有过一段移动开发的经历,较为关注这块且做为一个爱拾人牙慧的切图狗,现将一些他人成熟的优化方法总结以下:html

固然,全部的优化都是有场景,请根据实际的场景去选择最优的方案使用。前端

 

DOM 相关

DOM 天生就慢,以下比喻就很形象的解释了这样的关系。git

把 DOM 和 js(ECMAScript)各自想象为一座岛屿,它们之间用收费桥进行链接。ECMAScript 每次访问 DOM,都要途径这座桥,并交纳“过桥费”。访问 DOM 的次数越多,费用也就越高。github

最基本的优化思路就是优化 DOM 的读写操做。web

减小对 DOM 元素读操做:

缓存 DOM 引用

获取 DOM 以后请将引用缓存,不要重复获取。不少人在使用 jQuery 的时候没有培养良好的习惯,链式调用用起来方便,但有时候会让人调入忽视缓存 DOM 的陷阱,由于获取太便捷就不去珍惜了,果真被偏心的就会有恃无恐。后端

var render = (function() {
    // get DOM 
    var domCache = document.querySelector("dom");
    
    return function() {
        // do something...
        domCache.style.width =  '100px';
        domCache.style.height = '100px';
        // ....
    }
})();

缓存 DOM 的属性

思路同上,在获取初始值后而且已知变化量,直接经过计算得知元素变化后的值并缓存在内存中,避免将结果值使用 DOM 属性进行存储。能够减小不少没必要要的 DOM 读取操做,特别是某些属性还会引起浏览器回流(这些属性下文会说起)。这在用 JavaScript 控制一些物体位置变化的时候比较容易忽略。jQuery 时代,人们习惯于将数据保存在 DOM 元素上,却不知这将引起性能问题,我曾今就犯过相似的错误,致使一个移动端上的赛车游戏性能低下。浏览器

// bad
var dom = document.querySelector("dom");
var step = 1;

var timer = window.setInterval(function () {
    var left = parseInt(dom.style.left);
    if (left >= 200) {
        clearInterval(timer);
    }
    dom.style.left = (left +1) + 'px';
}, 1000 / 60);

// good
var dom = document.querySelector("dom");
var step = 1;
var left = parseInt(dom.style.left);

var timer = window.setInterval(function () {
    if (left >= 200) {
        clearInterval(timer);
    }
    left++;
    dom.style.left = left + 'px';
}, 1000 / 60);

还有常见的就是缓存 HTMLCollection 的 length,HTMLCollection 还有一个很重要的特性就是它是根据页面的状况动态更新的,若是你更新的页面那么它的内容也会发生变化,下面的代码会是无限循环。缓存

var divs = document.getElementsByTagName("div") ;
for(var i = 0 ; i < divs.length ; i ++){
    document.body.appendChild(document.createElement("div")) ;
}

减小 DOM 的写操做

记录上次结果与现有结果 Diff, 若有变化才进行写操做,去除没必要要的写操做。app

var dom = document.querySelector('#dom');
var lastVal =  null;
var currVal = null;

if (lastVal !== currVal) {
    dom.style.someAttr = currVal;
}

避免循环操做 DOM 元素

循环中操做 DOM,每次循环都会产生一次读操做与写操做,因此咱们的优化思路是将循环结果缓存起来,循环结束后统一操做能节省不少读写操做。

合并屡次写操做

// bad
for (var i = 0; length < 100; i++) {
    // 一次 get,一次 set
    document.getElementById('text').innerHTML += `text${i}`
}

// better
var html = '';

for (var i = 0; length < 100; i++) {
    html += `text${i}`
}

document.getElementById('text').innerHTML = html;

使用 documentFragment

另外 documentFragment 也可达到这样的目的,由于文档片断存在于内存中,并不在 DOM 树中,因此将子元素插入到文档片断时不会引发页面回流。所以,使用文档片断 document fragments 一般会起到优化性能的做用。

var fragment = document.createDocumentFragment();

for (var i = 0; length < 100; i++) {
    var div = document.createElement('div');
    div.innerHTML = i;
    fragment.appendChild(div);
}

document.body.appendChild(fragment)

至于上文中 innerHTML 与 fragment 谁更快,请看这里,有此文还引伸出新的优化规则:优先使用 innerHTML(甚至是更好地 insertAdjacentHTML) 与 fragment

回流(reflow)与重绘(repaint)

若是了解过浏览器的渲染原理,咱们知道,重绘和回流的性能消耗是很是严重的,破坏用户体验,形成UI卡顿。回流也叫重排,回流必定会引发重绘,重绘不必定会触发回流。触发浏览器回流与重绘的条件有:

  • 添加或者删除可见的DOM元素
  • 元素位置改变
  • 元素尺寸改变
  • 元素内容改变
  • 页面渲染初始化
  • 浏览器窗口尺寸改变,字体大小改变,页面滚动

咱们的优化思路是减小甚至避免触发浏览器产生回流与重绘。

避免一些引发浏览器回流的属性

当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括:

  • Element:
    • offsetTopoffsetLeftoffsetWidthoffsetHeight
    • scrollTopscrollLeftscrollWidthscrollHeight
    • clientTopclientLeftclientWidthclientHeight
  • Frame, HTMLImageElement:
    • heightwidth
  • Range:
    • getBoundingClientRect(),
    • getClientRects()
  • SVGLocatable:

    • computeCTM()
    • getBBox()
  • SVGTextContent:

    • getCharNumAtPosition()
    • getComputedTextLength()
    • getEndPositionOfChar()
    • getExtentOfChar()
    • getNumberOfChars()
    • getRotationOfChar()
    • getStartPositionOfChar()
    • getSubStringLength()
    • selectSubString()
  • SVGUse:

    • instanceRoot
  • window:

    • getComputedStyle()
    • scrollBy()scrollTo()scrollXscrollY
    • webkitConvertPointFromNodeToPage()webkitConvertPointFromPageToNode()

更全面的属性请访问这个Gist

display:none的元素上进行操做

若是 DOM 元素上须要进行不少操做,可让该 DOM 元素从 DOM 树中"离线"——display:none,等操做完毕后再”上线“取消display:none。这样能去除在操做期间引起的回流与重绘。

操做 cloneNode

也能够将当前节点克隆一份,操做克隆节点,操做完毕以后再替换原节点。

浏览器优化

重排和重绘很容易被引发,并且重排的花销也不小,若是每句 JavaScript 操做都去重排重绘的话,浏览器可能就会受不了。因此不少浏览器都会优化这些操做,浏览器会维护一个队列,把全部会引发重排、重绘的操做放入这个队列,等队列中的操做到了必定的数量或者到了必定的时间间隔,浏览器就会 flush 队列,进行一个批处理。这样就会让屡次的重排、重绘变成一次重排重绘。

var dom = document.querySelector("#dom");

// 触发两次 layout
var newWidth = dom.offsetWidth + 10;   // Read  
aDiv.style.width = newWidth + 'px';     // Write  
var newHeight = dom.offsetHeight + 10; // Read  
aDiv.style.height = newHeight + 'px';   // Write

// 只触发一次 layout
var newWidth = dom.offsetWidth + 10;   // Read  
var newHeight = dom.offsetHeight + 10; // Read  
aDiv.style.width = newWidth + 'px';     // Write  
aDiv.style.height = newHeight + 'px';   // Write

一次性修改元素

每次修改 DOM 元素,均可能引发浏览器的回流与重绘,尽量去较少改变次数,这与上文优化 DOM 读写思路重合再也不赘述。

经过样式去改变元素样式
// bad
var dom = document.getElementById('dom');
dom.style.color = '#FFF';
dom.style.fontSize = '12px';
dom.style.width = '200px';

上述例子每次修改 style 属性后都会触发元素的重绘,若是修改了的属性涉及大小和位置,将会致使回流。因此咱们应当尽可能避免屡次为一个元素设置 style 属性,应当经过给其添加新的 CSS 类,来修改其样式。

<!--better-->
<style>
.my-style {
    color: #FFF;
    font-size: 12px;
    width: 200px;
}
</style>

<script>
    var dom = document.getElementById('dom');
    dom.classList.add('my-style');
</script>
cssText

同上文优化思路,用cssText也可达到相似目的。

var dom = document.getElementById('dom');
 dom.style.cssText = 'color: #FFF;font-size: 12px;width: 200px;'

简化 DOM 结构

首先每一个 DOM 对象的都会占据浏览器资源,占据的资源与数量成正相关。另外,DOM 结构越深,最里面 DOM 元素的变化可能引起的祖先 DOM 数量就越多。

使用场景例如大量数据表格的展现,几万个 DOM 就能把浏览器卡得不要不要的甚至直接奔溃。我曾经遇到这样真实的案例,后在保持后端接口不变的状况下,采用前端假分页解决。

DOM 事件优化

使用事件委托或事件代理

使用事件代理与每一个元素都绑定事件相比,可以节省更多的内存。固然还有另外的好处,就是新增长假的 DOM 元素也无需绑定事件了,这里不详述。

截流函数

首先这样场景下,在页面滚动的时候需根据页面滚动位置作一些操做,可是 scroll 事件触发过于频繁,致使绑定的事件执行频率过高开销太大。咱们就须要采起一些措施来下降事件被执行的频率。

节流实际上就下降函数触发的频率。

let throttle = (func, wait) => {
    let context, args;
    let previous = 0;

    return function () {
        var now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    };
};

防抖函数

说道节流,不得不提防抖,相交于节流的下降触发的频率,防抖函数其实是延后函数执行的时机,通常状况下,防抖比截流更节省性能。

let debounce = (func, wait) => {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(function () {
            func.apply(context, args)
        }, wait);
    };
};

使用场景例如一个输入框的实时搜索,对用户而言其实想要输入的关键词是输入完成的最终结果,而程序须要实时针对用户输入的无效关键词进行响应,这无疑是种浪费。咱们须要

CSS

文档流中元素样式改变可能触发浏览器回流,被影响的 DOM 树越大,须要重绘的时间就越长,也就可能致使性能问题。CSS Triggers 就列举了会引起浏览器,回流与重绘的属性。

使用定位让元素脱离文档流

使用定位让元素脱离文档流,引起回流重绘的 DOM 树范围被大大缩小。

.selector {
    position: fixed;
    // or
    position: absolute; 
}

使用 transform 与 opacity

transform 和 opacity 保证了元素属性的变化不影响文档流、也不受文档流影响,而且不会形成重绘。

FLTP

FLIP 来源于 First,Last,Invert,Play。FLIP 是将一些开销高昂的动画,如针对 widthheightleft 或 top 的动画,映射为 transform 动画。经过记录元素的两个快照,一个是元素的初始位置(First – F),另外一个是元素的最终位置(Last – L),而后对元素使用一个 transform 变换来反转(Invert – I),让元素看起来还在初始位置,最后移除元素上的 transform 使元素由初始位置运动(Play – P)到最终位置。

触发 GPU 加速

使用 GPU 硬件加速可使得浏览器动画更加流畅,不过切勿贪杯, GPU 加速是损耗硬件资源为代价的,会致使移动端设备续航能力的下降。

.selectror {
    webkit-transform: translateZ(0);
    -moz-transform: translateZ(0);
    -ms-transform: translateZ(0);
    -o-transform: translateZ(0);
    transform: translateZ(0);
}
// 或者
.selector {
    webkit-transform: translate3d(0,0,0);
    -moz-transform: translate3d(0,0,0);
    -ms-transform: translate3d(0,0,0);
    -o-transform: translate3d(0,0,0);
    transform: translate3d(0,0,0);
}

transform 在浏览器中可能有一些非预期内的表现,好比闪烁等,可使用以下代码 hack:

.selector {
    -webkit-backface-visibility: hidden;
    -moz-backface-visibility: hidden;
    -ms-backface-visibility: hidden;
    backface-visibility: hidden;

    -webkit-perspective: 1000;
    -moz-perspective: 1000;
    -ms-perspective: 1000;
    perspective: 1000;
}

will-change

上一种方式实际上是欺骗浏览器,达到浏览器“误觉得”须要 GPU 渲染加速,而 will-change 则是很礼貌的告知浏览器“这里会变化,请先作好准备”。不过切勿贪杯,适度使用。

.selector {
    will-change: auto
    will-change: scroll-position
    will-change: contents
    will-change: transform        // Example of <custom-ident> 
    will-change: opacity          // Example of <custom-ident>
    will-change: left, top        // Example of two <animateable-feature>
    
    will-change: unset
    will-change: initial
    will-change: inherit
}

避免复杂的 CSS 选择器以及 calc

复杂的 CSS 选择器会致使浏览器做大量的计算,咱们应当避免

.box:nth-last-child(-n+1) .title {
  /* styles */
}

避免动画中使用使用高开销的CSS属性

CSS 有些属性存性能问题,使用它们会致使浏览器进行大量计算,特别是在 animation 中,咱们应该谨慎使用,

  • box-shaow
  • background-image:
  • filter
  • border-radius
  • transforms
  • filters

使用 flexbox 布局替代 浮动布局

新版 flexbox 通常比旧版 flexbox 或基于浮动的布局模型更快

Canvas

对于渲染频率不一致的场景,采用分屏绘制

有些动画场景好比游戏中,背景通常变化较游戏物体运动较少,咱们就能够把这些跟新频率较低的物体分离出造成一个更新频率更低的 Canvas 层。

帧率与帧生成

帧率或帧率是用于测量显示帧数的量度。测量单位为“每秒显示帧数”(Frame per Second,FPS)或“赫兹”,通常来讲 FPS 用于描述视频、电子绘图或游戏每秒播放多少幀。
via Wikipedia

上文说了那么多,其实都是在为人眼的感觉服务。通常来讲电影帧率每秒 24 帧,对通常人而言已算可接受了。可是游戏与页面动效追求 60 帧乃至更高,由于电影画面是预先处理过的,运动画面中包含了画面运动信息 —— 也就是咱们人眼看快速运动的物体产生的模糊感,人脑会根据这些模糊感去脑补画面的运动感。而游戏或者交互动画不少是实时绘制出来的,并不包含模糊人脑天然也没法脑补了,因此对帧率更加苛刻,这也是为何有些游戏会有动态模糊弥补游戏帧率不足来改善游戏观感这个选项了。

使用微任务分解大量计算

除了人们关注的帧率,帧生成时间也很重要。假使帧率过关可是生成时间不够恒定,就容易产生跳帧感,就比如一锅粥里的老鼠屎。解决方法就是分解高计算量的操做,维护成任务列表平均分布到刷新间隔中去执行。谢谢聂俊在讲解游戏刷新率的启发,玩游戏也能学知识!哎呀,串场了这是机核的口号~~

使用 requestAnimationFrame

相比 setTimeOut,setInterval 恒定间隔刷新方案,requestAnimationFrame 能充分利用显示器的刷新机制,与浏览器的刷新频率保持同步,带来更加流畅的动画。

另外使用 requestAnimationFrame 页面处于非激活状态,动画也会中止执行,这样更加节省机器能耗。

Web Worker

JavaScript 是单线程的,大量的计算会阻塞线程,致使浏览器丢帧。Web Worker 给 JavaScript 开辟新的线程的能力,咱们能够将纯计算在 Web Worker 中处理。

相关文章
相关标签/搜索