最近工做中一个项目在运行时有一些性能问题,为此我看了不少与性能优化相关的内容,下面作个简单的分享。javascript
前端性能优化,这包括 CSS/JS 性能优化、网络性能优化等等内容,这方面的内容 《高性能网站建设指南》、《高性能网站建设进阶指南》、《高性能JavaScript》 等等书都作了不少讲解,强烈推荐阅读。css
下面的内容,上面提到的书中大都包含了,所以能够考虑转而去读这些书,作一个完彻底全的了解,对于本文,也就不要再读下去了。html
若是你坚持看到了这里,那就来谈谈我遇到的一些前端性能问题,并聊聊解决方案。前端
优先优化对性能影响大的部分
当应用有了性能问题后,不要一股脑扎到代码中去,首先要想一想那部分对性能影响最大。优先优化那些对性能影响大的部分,能够起到立杆见影的效果。
使用 Chrome DevTools ,能够很快地找到致使性能变差的最主要因素,关于 Chrome DevTools 的使用强烈推荐阅读 Google Developers 上面的系列教程 - Chrome DevTools。java
另外在对代码进行优化的时候,也首先要关注那些存在循环或者高频调用的地方。有的时候咱们可能不知道某个地方是否会高频执行,好比某些事件的回调。这个时候可使用 console.count
来对执行次数进行统计。当这部分高频执行的代码已经足够优化的时候,就要考虑是否可以减小执行次数。好比一个时间复杂度为 O(n*n*n)
的算法,再怎么优化也不如将其变为 O(n*n)
来的快。node
对高频触发的事件进行节流或消抖
对于 Scroll 和 Touchmove 这类事件,永远不要低估了它们的执行频率,处理这类事件的时候能够考虑是否要给它们添加一个节流或者消抖过的回调。节流和消抖,可能其余人不这么翻译,其实也就是 debounce
和 throttle
这两个函数。webpack
debounce
和 throttle
是两个类似(但不相同)的用于控制函数在某段事件内的执行频率的技术。你能够在 underscore 或者 lodash 中找到这两个函数。git
使用 debounce
进行消抖
屡次连续的调用,最终实际上只会调用一次。想象本身在电梯里面,门将要关上,这个时候另一我的来了,取消了关门的操做,过了一下子门又要关上,又来了一我的,再次取消了关门的操做。电梯会一直延迟关门的操做,直到某段时间里没人再来。github
因此 debounce
适合用在好比对用户输入内容进行校验的这种场景下,屡次触发只须要响应最后一次触发就行了。web
使用 throttle
进行节流
将频繁调用的函数限定在一个给定的调用频率内。它保证某个函数频率再高,也只能在给定的事件内调用一次。好比在滚动的时候要检查当前滚动的位置,来显示或隐藏回到顶部按钮,这个时候可使用 throttle
来将滚动回调函数限定在每 300ms 执行一次。
须要提到的是,这两个函数经常被误用,且不少时候当事人并无意识到本身误用了。我曾经用错过,也见过别人用错。这两个函数都接受一个函数做为参数,而后返回一个节流/去抖后的函数,下面第二种用法才是正确的用法:
1 |
// 错误的用法,每次事件触发都获得一个新的函数 |
JavaScript 很快,DOM 很慢
JavaScript 现在已经很快了,真正慢的是 DOM。所以避免使用一些不易读但听说能提升速度的写法。不久前,
一位朋友对我说使用 ‘+’ 号将字符串转为数字比使用 parseInt
快。对此我并无怀疑,由于直觉上 parseInt 进行了函数调用,极可能会慢一些,咱们一块儿在 node v6.3.0 上进行了一些验证,结果的确如咱们所预计的那样,可是差异有多大呢,进行了 5 亿次迭代,使用 +
号的方法仅仅快了2秒。虽然快了两秒,但实际中将字符转为数字的操做可能只会进行几回,所以这样的作法根本没有意义,它只会让代码变得更难读。
1 |
plus: 1694.392ms |
真正慢的是 DOM,DOM 对外提供了 API,而 JavaScript 能够调用这些 API,它们二者就像是使用一座桥梁相连,每次过桥都要被收取大量费用,所以应该尽可能让减小过桥的次数。
为何 DOM 很慢
谈到这里须要对浏览器利用 HTML/CSS/JavaScript 等资源呈现出精彩的页面的过程进行简单说明。浏览器在收到 HTML 文档以后会对文档进行解析开始构建 DOM (Document Object Model) 树,进而在文档中发现样式表,开始解析 CSS 来构建 CSSOM(CSS Object Model)树,这二者都构建完成后,开始构建渲染树。整个过程以下:
在每次修改了 DOM 或者其样式以后都要进行 DOM树的构建,CSSOM 的从新计算,进而获得新的渲染树。浏览器会利用新的渲染树对页面进行重排和重绘,以及图层的合并。一般浏览器会批量进行重排和重绘,以提升性能。但当咱们试图经过 JavaScript 获取某个节点的尺寸信息的时候,为了得到当前真实的信息,浏览器会马上进行一次重排。
避免强制性同步布局
在 JavaScript 中读取到的布局信息都是上一帧的信息,若是在 JavaScript 中修改了页面的布局,好比给某个元素添加了一个类,而后再读取布局信息。这个时候为了得到真实的布局信息,浏览器须要强制性对页面进行布局。所以应该避免这样作。
批量操做 DOM
在必需要进行频繁的 DOM 操做时,可使用 fastdom 这样的工具,它的思路是将对页面的读取和改写放进队列,在页面重绘的时候批量执行,先进行读取后改写。由于若是将读取与改写交织在一块儿可能引发屡次页面的重排。而利用 fastdom 就能够避免这样的状况发生。
虽然有了 fastdom 这样的工具,但有的时候仍是不能从根本上解决问题,好比我最近遇到的一个状况,与页面简单的一次交互(轻轻滚动页面)就执行了几千次 DOM 操做,这个时候核心要解决的是减小 DOM 操做的次数。这个时候就要从代码层面考虑,看看是否有没必要要的读取。
另一些关于高效操做 DOM 的方法,能够参见《高性能 JavaScript》相关章节,也能够先参考一下个人读书笔记 《高性能 JavaScript》
优化渲染性能
浏览器一般每秒更新页面 60 次,每一帧的时间就是 16.6ms,为了能让浏览器保持 60帧 的帧率,为了让动画看起来流畅,须要保证帧率达到 60fps,所以每一帧的逻辑须要在 16.6ms 内完成。
每一帧实际上都包含下列步骤:
所以,一般 JavaScript 的执行时间不能超过 10ms。
- JavaScript:改变元素样式,添加元素到 DOM 中等等
- Style:元素的类或者style改变了,这个时候须要从新计算元素的样式
- Layout:须要从新计算元素的具体尺寸
- Paint:将元素的绘制的图层上
- Composite:合并多个图层
固然也不是说每一帧都会进行这些操做。当你的 JavaScript 改变了某个 layout 属性,好比元素的 width
和 height
或者 top
等等,浏览器就会从新计算布局,并对整个页面进行重排。
若是修改了 background
、color
这样的仅仅会让页面重绘的属性,这不会影响页面的布局,浏览器会跳过计算布局(layout)的过程,只进行重绘(paint)。
若是修改了一个不须要计算布局也不须要重绘的属性,那就只会进行图层的合并,这是代价最小的修改。从 https://csstriggers.com/ 上你能够知道修改那些样式属性会触发(Layout,Paint,Composite)中的那些操做。
将渐变或者会动画元素放到单独的绘制层中
绘制并不是在一个单独的画布上进行的,而是多层。所以将那些会变更的元素提高至单独的图层,可让他的改变影响到的元素更少。
可使用 CSS 中的 will-change: transform;
或者 transform: translateZ(0);
这样来将元素提高至单独的图层中。
在调试的时候你能够在 Chrome DevTools 的 timeline 面板来观察绘制图层。固然也不是说图层越多越好,由于新增长一个图层可能会耗费额外的内存。且新增长一个图层的目的是为了不某个元素的变更影响其余元素。
下降绘制复杂度
某些属性的重绘相对而言更加复杂,好比 filter、box-shadow 等滤镜或渐变效果。所以不要滥用这类效果。
优化 JavaScript 的执行
下面提到的 JavaScript 优化,并非说如何让 JavaScript 执行的更快,而是如何让 JavaScript 更高效地与 DOM 配合。
使用 requestAnimationFrame
来更新页面
咱们但愿在每一帧刚开始的时候对页面进行更改,目前只有使用 requestAnimationFrame
可以保证这一点。使用 setTimeout
或者 setInterval
来触发更新页面的函数,该函数可能在一帧的中间或者结束的时间点上调用,进而致使该帧后面须要进行的事情没有完成,引起丢帧。
requestAnimationFrame
会将任务安排在页面重绘以前,这保证动画能有足够的时间来执行 JavaScript 。
使用 Web Worker 来处理复杂的计算
JavaScript 是在单线程的,而且可能会一直这样,所以 JavaScript 在执行复杂计算的时候极可能会阻塞线程,致使页面假死。但 Web Worker 的出现,以另一种方式给了咱们多线程的能力,能够将复杂计算放在 worker 中进行,当计算完成后,以 postMessage
的形式将结果传回来。
对于单个函数,由于 Web Worker 接受一个脚本的 url 做为参数,使用 URL.createObjectURL
方法,咱们能够将一个函数的内容转换为 url,利用它建立一个 worker。
1 |
var workerContent = ` |
使用 transform 和 opacity 来完成动画
现在只有对这两个属性的修改不须要经历 layout 和 paint 过程。
优化 CSS
CSS 选择器在匹配的时候是由右至左进行的,所以最后一个选择器常被称为关键选择器,由于最后一个选择越特殊,须要进行匹配的次数越少。要千万避免使用 *
(通用选择器)做为关键选择器。由于它能匹配到全部元素,进而倒数第二个选择器还会和全部元素进行一次匹配。这致使效率很低下。
1 |
/* 不要这样作 */ |
另外 first-child
这类伪类选择器也不够特殊,也要避免将它们做为关键选择器。关键选择器越特殊,浏览器就能用较少的匹配次数找到待匹配元素,选择器性能也就越好。
还有一个老生常谈的注意事项,不要使用太多的选择器。若是还有同窗很悲剧地要兼容低版本 IE,要避免使用 CSS 表达式,它的性能不好,详细内容可参见我以前记录的一篇笔记 《高性能网站建设指南》笔记
合理处理脚本和样式表
现在有了 requirejs,webpack 等工具,可能不多会在页面中加载不少 JavaScript/CSS 代码了。尽管如此,仍是有必要谈谈如何合理处理脚本和样式表。
大多数人已经知道一般要把 JavaScript 放在文档底部,把 CSS 放在文档顶部。为何呢?由于 JavaScript 会阻塞页面的解析,而外部样式表会阻塞页面的呈现和 JavaScript 的执行。
CSS阻塞渲染
一般状况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成以前,页面不会被渲染,放在顶部让样式表可以尽早开始加载。但若是把引入样式表的 link 放在文档底部,页面虽然能马上呈现出来,可是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会当即进行重绘,这也就是一般所说的闪烁了。
JavaScript 阻塞文档解析
当在 HTML 文档中遇到 script 标签后控制权将交给 JavaScript,在 JavaScript 下载并执行完成以前,都不会解析 HTML。所以若是将 JavaScript 放在文档顶部,刚好这个时候 JavaScript 脚本加载的特别慢,用户将会等待很长一段时间,这段个时候 HTML 文档尚未解析到 body 部分,页面会是空白的。
另外经常被忽略的事实是:在浏览器没有下载并解析完成使用 link 引入的 CSS 文件以前,JavaScript 是不会执行的,由于 JavaScript 中可能须要读取样式,而此时样式表尚未加载回来,所以浏览器不会执行 JavaScript。能够给 JavaScript 加上 async 标记,表示 JavaScript 的执行不会读取 DOM ,JavaScript 能够不被 CSS 阻塞,能够在空闲时间马上执行。
综上所述,你更要保证 CSS 文件加载的足够快。
关于这部份内容, 《高性能网站建设指南》 上有很精彩的讲解,墙裂推荐。《高性能网站建设指南》我在读的时候记录了笔记,能够在这里看到。
最后强烈推荐阅读 Google Developers 中关于性能优化的系列文章。
参考资料
- 《高性能网站建设指南》
- 《高性能网站建设进阶指南》
- 《高性能JavaScript》
- Google Developers
- Efficient JavaScript
- Best Practices for Speeding Up Your Web Site
本文章转自:http://ymfe.tech/blog/2016-09-24-fe-performance-optimization/
分享技术,分享快乐!