原文连接:bluest.mecss
前端优化是个很普遍的命题,铺开去得出本书了(事实上我也没那本事),实际上市面上也有不少相关的书籍。动画与交互上的性能问题最容易被察觉,特别是在机能较低的移动端。因为本身有过一段移动开发的经历,较为关注这块且做为一个爱拾人牙慧的切图狗,现将一些他人成熟的优化方法总结以下:html
固然,全部的优化都是有场景,请根据实际的场景去选择最优的方案使用。前端
DOM 天生就慢,以下比喻就很形象的解释了这样的关系。git
把 DOM 和 js(ECMAScript)各自想象为一座岛屿,它们之间用收费桥进行链接。ECMAScript 每次访问 DOM,都要途径这座桥,并交纳“过桥费”。访问 DOM 的次数越多,费用也就越高。github
最基本的优化思路就是优化 DOM 的读写操做。web
获取 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 读取操做,特别是某些属性还会引起浏览器回流(这些属性下文会说起)。这在用 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")) ; }
记录上次结果与现有结果 Diff, 若有变化才进行写操做,去除没必要要的写操做。app
var dom = document.querySelector('#dom'); var lastVal = null; var currVal = null; if (lastVal !== currVal) { dom.style.someAttr = currVal; }
循环中操做 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
。
若是了解过浏览器的渲染原理,咱们知道,重绘和回流的性能消耗是很是严重的,破坏用户体验,形成UI卡顿。回流也叫重排,回流必定会引发重绘,重绘不必定会触发回流。触发浏览器回流与重绘的条件有:
咱们的优化思路是减小甚至避免触发浏览器产生回流与重绘。
当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
height
、width
getBoundingClientRect()
,getClientRects()
SVGLocatable:
computeCTM()
getBBox()
SVGTextContent:
getCharNumAtPosition()
getComputedTextLength()
getEndPositionOfChar()
getExtentOfChar()
getNumberOfChars()
getRotationOfChar()
getStartPositionOfChar()
getSubStringLength()
selectSubString()
SVGUse:
instanceRoot
window:
getComputedStyle()
scrollBy()
、scrollTo()
、scrollX
、scrollY
webkitConvertPointFromNodeToPage()
、webkitConvertPointFromPageToNode()
更全面的属性请访问这个Gist
display:none
的元素上进行操做若是 DOM 元素上须要进行不少操做,可让该 DOM 元素从 DOM 树中"离线"——display:none
,等操做完毕后再”上线“取消display:none
。这样能去除在操做期间引起的回流与重绘。
也能够将当前节点克隆一份,操做克隆节点,操做完毕以后再替换原节点。
重排和重绘很容易被引发,并且重排的花销也不小,若是每句 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
也可达到相似目的。
var dom = document.getElementById('dom'); dom.style.cssText = 'color: #FFF;font-size: 12px;width: 200px;'
首先每一个 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); }; };
使用场景例如一个输入框的实时搜索,对用户而言其实想要输入的关键词是输入完成的最终结果,而程序须要实时针对用户输入的无效关键词进行响应,这无疑是种浪费。咱们须要
文档流中元素样式改变可能触发浏览器回流,被影响的 DOM 树越大,须要重绘的时间就越长,也就可能致使性能问题。CSS Triggers 就列举了会引起浏览器,回流与重绘的属性。
使用定位让元素脱离文档流,引起回流重绘的 DOM 树范围被大大缩小。
.selector { position: fixed; // or position: absolute; }
transform 和 opacity 保证了元素属性的变化不影响文档流、也不受文档流影响,而且不会形成重绘。
FLTP
FLIP 来源于 First,Last,Invert,Play。FLIP 是将一些开销高昂的动画,如针对
width
,height
,left
或top
的动画,映射为transform
动画。经过记录元素的两个快照,一个是元素的初始位置(First – F),另外一个是元素的最终位置(Last – L),而后对元素使用一个 transform 变换来反转(Invert – I),让元素看起来还在初始位置,最后移除元素上的 transform 使元素由初始位置运动(Play – P)到最终位置。
使用 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; }
上一种方式实际上是欺骗浏览器,达到浏览器“误觉得”须要 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 }
calc
复杂的 CSS 选择器会致使浏览器做大量的计算,咱们应当避免
.box:nth-last-child(-n+1) .title { /* styles */ }
CSS 有些属性存性能问题,使用它们会致使浏览器进行大量计算,特别是在 animation
中,咱们应该谨慎使用,
box-shaow
background-image:
filter
border-radius
transforms
filters
新版 flexbox 通常比旧版 flexbox 或基于浮动的布局模型更快
有些动画场景好比游戏中,背景通常变化较游戏物体运动较少,咱们就能够把这些跟新频率较低的物体分离出造成一个更新频率更低的 Canvas 层。
帧率或帧率是用于测量显示帧数的量度。测量单位为“每秒显示帧数”(Frame per Second,FPS)或“赫兹”,通常来讲 FPS 用于描述视频、电子绘图或游戏每秒播放多少幀。
via Wikipedia
上文说了那么多,其实都是在为人眼的感觉服务。通常来讲电影帧率每秒 24 帧,对通常人而言已算可接受了。可是游戏与页面动效追求 60 帧乃至更高,由于电影画面是预先处理过的,运动画面中包含了画面运动信息 —— 也就是咱们人眼看快速运动的物体产生的模糊感,人脑会根据这些模糊感去脑补画面的运动感。而游戏或者交互动画不少是实时绘制出来的,并不包含模糊人脑天然也没法脑补了,因此对帧率更加苛刻,这也是为何有些游戏会有动态模糊弥补游戏帧率不足来改善游戏观感这个选项了。
除了人们关注的帧率,帧生成时间也很重要。假使帧率过关可是生成时间不够恒定,就容易产生跳帧感,就比如一锅粥里的老鼠屎。解决方法就是分解高计算量的操做,维护成任务列表平均分布到刷新间隔中去执行。谢谢聂俊在讲解游戏刷新率的启发,玩游戏也能学知识!哎呀,串场了这是机核的口号~~
requestAnimationFrame
相比 setTimeOut
,setInterval
恒定间隔刷新方案,requestAnimationFrame
能充分利用显示器的刷新机制,与浏览器的刷新频率保持同步,带来更加流畅的动画。
另外使用 requestAnimationFrame
页面处于非激活状态,动画也会中止执行,这样更加节省机器能耗。
JavaScript 是单线程的,大量的计算会阻塞线程,致使浏览器丢帧。Web Worker 给 JavaScript 开辟新的线程的能力,咱们能够将纯计算在 Web Worker 中处理。