javascript 性能优化
1. 对高频触发的事件进行节流或消抖
对于 Scroll 和 TouchMove 这类事件,永远不要低估了它们的执行频率,处理这类事件的时候可以考虑是否要给它们添加一个节流或者消抖过的回调。节流和消抖,可能其他人不这么叫,其实也就是 debounce 和 throttle 这两个函数。
debounce 和 throttle 是两个相似(但不相同)的用于控制函数在某段事件内的执行频率的技术。你可以在 underscore 或者 lodash 中找到这两个函数。
1.1 使用 debounce 进行消抖
多次连续的调用,最终实际上只会调用一次。想象自己在电梯里面,门将要关上,这个时候另外一个人来了,取消了关门的操作,过了一会儿门又要关上,又来了一个人,再次取消了关门的操作。电梯会一直延迟关门的操作,直到某段时间里没人再来。
所以 debounce 适合用在比如对用户输入内容进行校验的这种场景下,多次触发只需要响应最后一次触发就好了。
1.2 使用 throttle 进行节流
将频繁调用的函数限定在一个给定的调用频率内。它保证某个函数频率再高,也只能在给定的事件内调用一次。比如在滚动的时候要检查当前滚动的位置,来显示或隐藏回到顶部按钮,这个时候可以使用 throttle 来将滚动回调函数限定在每 300ms 执行一次。
需要提到的是,这两个函数常常被误用,且很多时候当事人并没有意识到自己误用了。我曾经用错过,也见过别人用错。这两个函数都接受一个函数作为参数,然后返回一个节流/去抖后的函数,下面第二种用法才是正确的用法:
// 错误的用法,每次事件触发都得到一个新的函数
$(window).on('scroll', function() {
_.throttle(doSomething, 300);
});
// 正确的用法,将节流后的函数作为回调
$(window).on('scroll', _.throttle(doSomething, 200));
1.3 使用 requestAnimationFrame 来更新页面
我们希望在每一帧刚开始的时候对页面进行更改,目前只有使用 requestAnimationFrame 能够保证这一点。使用 setTimeout 或者 setInterval 来触发更新页面的函数,该函数可能在一帧的中间或者结束的时间点上调用,进而导致该帧后面需要进行的事情没有完成,引发丢帧。
requestAnimationFrame 会将任务安排在页面重绘之前,这保证动画能有足够的时间来执行 JavaScript 。
1.4 使用 Web Worker 来处理复杂的计算
JavaScript 是在单线程的,并且可能会一直这样,因此 JavaScript 在执行复杂计算的时候很可能会阻塞线程,导致页面假死。但 Web Worker 的出现,以另外一种方式给了我们多线程的能力,可以将复杂计算放在 worker 中进行,当计算完成后,以 postMessage 的形式将结果传回来。
对于单个函数,因为 Web Worker 接受一个脚本的 url 作为参数,使用 URL.createObjectURL 方法,我们可以将一个函数的内容转换为 url,利用它创建一个 worker。
var workerContent = `
self.onmessage = function(evt){
// ...
// 在这里进行复杂计算
var result = complexFunc();
// 将结果传回
self.postMessage(result);
};`
// 得到 url
var blob = new Blob([workerContent]);
var url = window.URL.createObjectURL(blob);
// 创建 worker
var worker = new Worker(url);
使用 transform 和 opacity 来完成动画
如今只有对这两个属性的修改不需要经历 layout 和 paint 过程。
避免在 scroll 或 touchmove 这类事件的回调中修改样式
因为 scroll 和 touchmove 事件的回调会在 requestAnimationFrame 之前触发,如果在这里面修改了样式,在 requestAnimationFrame 中若有读取样式的操作,就会强制重新计算样式。但是如果一定要修改样式该怎么办呢?可以将对样式的修改放在一个函数中,并使用 requestAnimationFrame 来触发。在 requestAnimationFrame 执行序列中,就算有对样式的修改,也不会触发重排,重排会在回调序列执行完成后进行。
更多关于使用 requestAnimationFrame 来提升性能的细节可以阅读这篇文章:Better Performance With requestAnimationFrame
2. 重绘与重排
浏览器在下载完成所有的资源之后,会建立两个内部的数据结构:
● DOM树:表示页面结构
● 渲染树:表示DOM节点如何显示
每当DOM元素的宽高,颜色等改变,都会触发渲染树的更新,对于尺寸变化会进行一次重新排版,然后在重新绘制。并不是任何操作都会触发重排,比如背景色的变化就只会触发一次重绘,因为布局并没有改变。
2.1 重排何时发生
● 添加或者删除可见元素
● 元素位置改变
● 元素尺寸改变
● 内容改变
● 页面渲染初始化
● 浏览器尺寸变化
● 渲染树变化的排队和刷新
由于每次重排都会产生计算消耗,所以浏览器会通过队列来批量执行重排过程。然而在读取元素的 offset_。scroll_,client* 等属性的时候,由于要获取到当前准确的信息,这个时候会强制进行重排以返回正确值。
2.2 最小化重绘与重排
2.2.1 批量改变样式
使用 el.style.cssText 来批量添加,或者在后面追加。
2.2.2 批量修改 DOM
当需要对 DOM 进行一系列操作的时候,可以采用下面的方式:
2.2.3 隐藏DOM
对其应用多次修改
重新显示该元素
其他一些方案:使用文档片段
2.2.4 缓存布局信息
因为在每次获取布局信息的时候都要进行渲染树的刷新,所以可以缓存下布局信息,避免频繁获取这些信息。
2.2.5 让元素脱离动画流
有的时候页面的顶部有一个动画,这个元素的高度不断变大,然后将下面的内容撑到下面,这个时候会导致整个页面的重绘,使得整个页面看起来一顿一顿的。解决方法是将这个元素在动画开始前设置为 绝对定位,让其脱离文档流,然后在应用动画,这个时候就只会对这个元素以及这个元素遮挡的元素进行重绘,在动画结束后再恢复定位让他回到标准流中。
2.2.6IE:hover
对于元素很多的时候,不要对大量元素使用 hover 选择器。在 IE8 中性能很差。
2.2.7 事件委托
利用事件冒泡的机制来处理大量的事件的绑定。
3. 使用 GPU 来加速页面渲染
前端工程师应该都听说过硬件加速,通常它是指利用 GPU 来加速页面的渲染。那么 GPU 目前在web页面的渲染过程中起到什么作用呢?
3.1 GPU 的作用
早期浏览器完全依赖 CPU 来进行页面渲染。现在随着 GPU 的能力增强和普及,且目前绝大多数运行浏览器的设备上都集成了 GPU。浏览器可以利用 GPU 来加速网页渲染。
GPU 包含几百上千个核心,但每个核心的结构都相对简单, GPU 的结构也决定了它适合用来进行大规模并行计算。进行图层合并需要操作大量的像素,这方面 GPU 能比 CPU 更高效的完成。这里有个视频,很清楚地说明 CPU 与 GPU 的差别。
3.2 页面渲染过程
浏览器利用 HTML 构建出 DOM 树,利用 CSS 构建 CSSOM 树,最终得到 Render 树。
然而这只是很宏观的描述,浏览器为了将 DOM 元素高效地绘制且正确地出来,将多个元素安排在一个图层中,使用 PaintLayer 来描述,在每个 PaintLayer 中又存在 GraphicsLayers。当某个元素的样式改变后,不需要去重绘某个图层就好了。
浏览器的每一帧都可能会经过以下几个步骤:
JavaScript 的执行可能修改 DOM 树和 CSSOM 树,随后浏览器需要重新计算样式,并根据新的样式计算出元素的实际属性(比如 CSS 中 width 是 50%,这里就要利用父元素的宽度得出自己真实的 width 值),重绘有变动的图层,随后将各图层传递给 GPU ,由 GPU 来进行图层的合并。
上面 5 个步骤中,Layout 和 Paint 是可以省略的,当修改后的样式不会改变元素的尺寸、位置等涉及布局的属性时候,就没有必要进行 Layout(计算布局),比如修改了 color 属性,这个时候就只需要进行重绘(Paint)步骤。同样的道理,修改某些属性也不需要进行 Paint 步骤,只需要 Composite 就可以。
因此,我们希望所做的操作能尽可能地避免 Layout 和 Paint 这两个步骤,这样一帧所需的时间也就会大大缩短,可以明显避免卡顿。
目前有三个属性的改变只需要进行 Composite 过程,分别是:
● filter
● transform
● opacity
这几个属性的改变,GPU 只需要在合并图层之前对图层进行一些变换,比如 opacity 属性的改变,GPU 只需要在合并之前改变图层的 alpha 通道。transform 和 filter 的改变 GPU 也可以利用矩阵变换很快地得到变化后的图层。
3.3 正确地利用 GPU
3.3.1 使用 transform, filter 和 opacity 来完成动画
使用以上 3 个属性来完成动画,可以避免在动画的每一帧进行重绘。如果在动画中改变了其他属性,那也不能避免重新绘制。
3.3.2 避免不合理地强制开启硬件加速
常常看到有文章指出使用 transform:translateZ(0); 这样的 hark 可以强制开启硬件加速来提高性能,这是错误的说法,要知道所谓的硬件加速就是利用 GPU 来将本就存在于 GPU 中的图层进行一些变换得到新的图层。如果改变的属性必须要要进行重绘,比如改变了 background 属性,那么图层还是要进行重绘然后重新加载至 GPU 中。这个时候就算强制开启硬件加速也没有什么用。
使用 transform:translateZ(0); 这样的 CSS hark 写法会将元素提升至单独的图层。在这么做之前要考虑为什么要这样做,创建新的图层的目的应该是,避免某个元素的改变导致大面积重绘,比如某个小标签的颜色的改变,导致大面积重绘,因此将其提升至单独的图层中。这里有个例子,小标签背景色的改变会导致大面积的重绘,但是如果将其提升至单独的图层后,改变它的背景色将只会重绘它自身。你可以代码 Chrome 调试工具,通过 Timeline 观察每次闪烁重绘的内容。
而如果整个图层的都要被重绘,那么再将其中的部分元素提升至单独的图层,会导致重绘的时候会分多个图层来进行绘制,然后在进行多个图层的合并,这个时候不如将所有元素放置在单个图层中,重绘整个大的图层。
4. 参考用链接
● scroll的优化 https://zhuanlan.zhihu.com/p/30078937?group_id=903286572552740864