前端性能优化

工作中一个项目在运行时有一些性能问题,为此我看了很多与性能优化相关的内容,下面做个简单的分享。

前端性能优化,这包括CSS/JS性能优化、网络性能优化等等内容,这方面的内容、等等书都做了很多讲解,强烈推荐阅读。(这些书单参见本文结尾)

下面的内容,上面提到的书中大都包含了,因此可以考虑转而去读这些书,做一个完完全全的了解,对于本文,也就不要再读下去了。

如果你坚持看到了这里,那就来谈谈我遇到的一些前端性能问题,并聊聊解决方案。

1优先优化对性能影响大的部分

当应用有了性能问题后,不要一股脑扎到代码中去,首先要想想那部分对性能影响最大。优先优化那些对性能影响大的部分,可以起到立杆见影的效果。

使用ChromeDevTools,可以很快地找到导致性能变差的最主要因素,关于ChromeDevTools的使用强烈推荐阅读GoogleDevelopers上面的系列教程-。

另外在对代码进行优化的时候,也首先要关注那些存在循环或者高频调用的地方。有的时候我们可能不知道某个地方是否会高频执行,比如某些事件的回调。这个时候可以使用console.count来对执行次数进行统计。当这部分高频执行的代码已经足够优化的时候,就要考虑是否能够减少执行次数。比如一个时间复杂度为O(n*n*n)的算法,再怎么优化也不如将其变为O(n*n)来的快。

2对高频触发的事件进行节流或消抖

对于Scroll和Touchmove这类事件,永远不要低估了它们的执行频率,处理这类事件的时候可以考虑是否要给它们添加一个节流或者消抖过的回调。节流和消抖,可能其他人不这么翻译,其实也就是debounce和throttle这两个函数。

debounce和throttle是两个相似(但不相同)的用于控制函数在某段事件内的执行频率的技术。你可以在underscore或者lodash中找到这两个函数。

2.1使用debounce进行消抖

多次连续的调用,最终实际上只会调用一次。想象自己在电梯里面,门将要关上,这个时候另外一个人来了,取消了关门的操作,过了一会儿门又要关上,又来了一个人,再次取消了关门的操作。电梯会一直延迟关门的操作,直到某段时间里没人再来。

所以debounce适合用在比如对用户输入内容进行校验的这种场景下,多次触发只需要响应最后一次触发就好了。

2.2使用throttle进行节流

将频繁调用的函数限定在一个给定的调用频率内。它保证某个函数频率再高,也只能在给定的事件内调用一次。比如在滚动的时候要检查当前滚动的位置,来显示或隐藏回到顶部按钮,这个时候可以使用throttle来将滚动回调函数限定在每300ms执行一次。

需要提到的是,这两个函数常常被误用,且很多时候当事人并没有意识到自己误用了。我曾经用错过,也见过别人用错。这两个函数都接受一个函数作为参数,然后返回一个节流/去抖后的函数,下面第二种用法才是正确的用法:

1

2

3

4

5

6

7

//错误的用法,每次事件触发都得到一个新的函数

$(window).on('scroll',function(){

_.throttle(doSomething,300);

});

//正确的用法,将节流后的函数作为回调

$(window).on('scroll',_.throttle(doSomething,200));

3JavaScript很快,DOM很慢

JavaScript如今已经很快了,真正慢的是DOM。因此避免使用一些不易读但据说能提高速度的写法。不久前,

一位朋友对我说使用‘+’号将字符串转为数字比使用parseInt快。对此我并没有怀疑,因为直觉上parseInt进行了函数调用,很可能会慢一些,我们一起在nodev6.3.0上进行了一些验证,结果的确如我们所预计的那样,但是差别有多大呢,进行了5亿次迭代,使用+号的方法仅仅快了2秒。虽然快了两秒,但实际中将字符转为数字的操作可能只会进行几次,因此这样的做法根本没有意义,它只会让代码变得更难读。

1

2

plus:1694.392ms

parseInt:3661.403ms

真正慢的是DOM,DOM对外提供了API,而JavaScript可以调用这些API,它们两者就像是使用一座桥梁相连,每次过桥都要被收取大量费用,因此应该尽量让减少过桥的次数。

3.1为什么DOM很慢

谈到这里需要对浏览器利用HTML/CSS/JavaScript等资源呈现出精彩的页面的过程进行简单说明。浏览器在收到HTML文档之后会对文档进行解析开始构建DOM(DocumentObjectModel)树,进而在文档中发现样式表,开始解析CSS来构建CSSOM(CSSObjectModel)树,这两者都构建完成后,开始构建渲染树。整个过程如下:

在每次修改了DOM或者其样式之后都要进行DOM树的构建,CSSOM的重新计算,进而得到新的渲染树。浏览器会利用新的渲染树对页面进行重排和重绘,以及图层的合并。通常浏览器会批量进行重排和重绘,以提高性能。但当我们试图通过JavaScript获取某个节点的尺寸信息的时候,为了获得当前真实的信息,浏览器会立刻进行一次重排。

3.2避免强制性同步布局

在JavaScript中读取到的布局信息都是上一帧的信息,如果在JavaScript中修改了页面的布局,比如给某个元素添加了一个类,然后再读取布局信息。这个时候为了获得真实的布局信息,浏览器需要强制性对页面进行布局。因此应该避免这样做。

3.3批量操作DOM

在必须要进行频繁的DOM操作时,可以使用这样的工具,它的思路是将对页面的读取和改写放进队列,在页面重绘的时候批量执行,先进行读取后改写。因为如果将读取与改写交织在一起可能引起多次页面的重排。而利用fastdom就可以避免这样的情况发生。

虽然有了fastdom这样的工具,但有的时候还是不能从根本上解决问题,比如我最近遇到的一个情况,与页面简单的一次交互(轻轻滚动页面)就执行了几千次DOM操作,这个时候核心要解决的是减少DOM操作的次数。这个时候就要从代码层面考虑,看看是否有不必要的读取。

4优化渲染性能

浏览器通常每秒更新页面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)。

如果修改了一个不需要计算布局也不需要重绘的属性,那就只会进行图层的合并,这是代价最小的修改。从上你可以知道修改那些样式属性会触发(Layout,Paint,Composite)中的那些操作。

4.1将渐变或者会动画元素放到单独的绘制层中

绘制并非在一个单独的画布上进行的,而是多层。因此将那些会变动的元素提升至单独的图层,可以让他的改变影响到的元素更少。

可以使用CSS中的will-change:transform;或者transform:translateZ(0);这样来将元素提升至单独的图层中。

在调试的时候你可以在ChromeDevTools的timeline面板来观察绘制图层。当然也不是说图层越多越好,因为新增加一个图层可能会耗费额外的内存。且新增加一个图层的目的是为了避免某个元素的变动影响其他元素。

4.2降低绘制复杂度

某些属性的重绘相对而言更加复杂,比如filter、box-shadow等滤镜或渐变效果。因此不要滥用这类效果。

5优化JavaScript的执行

下面提到的JavaScript优化,并不是说如何让JavaScript执行的更快,而是如何让JavaScript更高效地与DOM配合。

5.1使用requestAnimationFrame来更新页面

我们希望在每一帧刚开始的时候对页面进行更改,目前只有使用requestAnimationFrame能够保证这一点。使用setTimeout或者setInterval来触发更新页面的函数,该函数可能在一帧的中间或者结束的时间点上调用,进而导致该帧后面需要进行的事情没有完成,引发丢帧。

requestAnimationFrame会将任务安排在页面重绘之前,这保证动画能有足够的时间来执行JavaScript。

5.2使用WebWorker来处理复杂的计算

JavaScript是在单线程的,并且可能会一直这样,因此JavaScript在执行复杂计算的时候很可能会阻塞线程,导致页面假死。但WebWorker的出现,以另外一种方式给了我们多线程的能力,可以将复杂计算放在worker中进行,当计算完成后,以postMessage的形式将结果传回来。

对于单个函数,因为WebWorker接受一个脚本的url作为参数,使用URL.createObjectURL方法,我们可以将一个函数的内容转换为url,利用它创建一个worker。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

varworkerContent=`

self.onmessage=function(evt){

//...

//在这里进行复杂计算

varresult=complexFunc();

//将结果传回

self.postMessage(result);

};`

//得到url

varblob=newBlob([workerContent]);

varurl=window.URL.createObjectURL(blob);

//创建worker

varworker=newWorker(url);

5.3使用transform和opacity来完成动画

如今只有对这两个属性的修改不需要经历layout和paint过程。

6优化CSS

CSS选择器在匹配的时候是由右至左进行的,因此最后一个选择器常被称为关键选择器,因为最后一个选择越特殊,需要进行匹配的次数越少。要千万避免使用*(通用选择器)作为关键选择器。因为它能匹配到所有元素,进而倒数第二个选择器还会和所有元素进行一次匹配。这导致效率很低下。

1

2

/*不要这样做*/

divp*{}

另外first-child这类伪类选择器也不够特殊,也要避免将它们作为关键选择器。关键选择器越特殊,浏览器就能用较少的匹配次数找到待匹配元素,选择器性能也就越好。

还有一个老生常谈的注意事项,不要使用太多的选择器。如果还有同学很悲剧地要兼容低版本IE,要避免使用CSS表达式,它的性能很差,详细内容可参见我之前记录的一篇笔记(Https://Github.com/wy-ei/notebook/issues/15)

7合理处理脚本和样式表

如今有了requirejs,webpack等工具,可能很少会在页面中加载很多JavaScript/CSS代码了。尽管如此,还是有必要谈谈如何合理处理脚本和样式表。

大多数人已经知道通常要把JavaScript放在文档底部,把CSS放在文档顶部。为什么呢?因为JavaScript会阻塞页面的解析,而外部样式表会阻塞页面的呈现和JavaScript的执行。

7.1CSS阻塞渲染

通常情况下CSS被认为是阻塞渲染的资源,在CSSOM构建完成之前,页面不会被渲染,放在顶部让样式表能够尽早开始加载。但如果把引入样式表的link放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,这也就是通常所说的闪烁了。

7.2JavaScript阻塞文档解析

当在HTML文档中遇到script标签后控制权将交给JavaScript,在JavaScript下载并执行完成之前,都不会解析HTML。因此如果将JavaScript放在文档顶部,恰好这个时候JavaScript脚本加载的特别慢,用户将会等待很长一段时间,这段个时候HTML文档还没有解析到body部分,页面会是空白的。

另外常常被忽略的事实是:在浏览器没有下载并解析完成使用link引入的CSS文件之前,JavaScript是不会执行的,因为JavaScript中可能需要读取样式,而此时样式表还没有加载回来,因此浏览器不会执行JavaScript。可以给JavaScript加上async标记,表示JavaScript的执行不会读取DOM,JavaScript可以不被CSS阻塞,可以在空闲时间立刻执行。

综上所述,你更要保证CSS文件加载的足够快。

关于这部分内容,上有很精彩的讲解,墙裂推荐。《高性能网站建设指南》我在读的时候记录了笔记,可以在看到。

最后强烈推荐阅读GoogleDevelopers中关于性能优化的(https://developers.google.com/web/fundamentals/performance)。

原文链接:http://www.kubiji.cn/juhe-id7101.html

相关推荐