前端性能优化

前段时间稍微了解了一些关于性能优化的几个点,所以整理以下,基本上参考了很多的博文,这里算是对一些资源的汇总,原创的东西并不多

##一、减少http请求
在一个网页的下载过程中,浏览器需要一个个去下载其中的组件,而这需要发起一个个的http请求来获得,对于一个http的请求所花费的时间包括了:建立连接的时间,发送请求的时间,等待服务器响应的时间,接收数据的时间,在《高性能网站建设》一书中提到了黄金法则就指明“只有10%~20%的最终用户相应时间花在了下载HTML文档上,其他都是花在了下载页面中的所有组件上”,所以尽量减少http的请求对于网页或者是App的优化有着很大的影响。具体的可以从如下两个方面入手:

###1.1 前端的优化
(1)使用CSS Sprites技术,将一些比较小的Icon等等之类的图片合并为一张图片,最后采用CSS中的background-position属性来定位具体的图标,从而将原来需要下载多个图标的http请求最后变为一个。适用于那些小的图标以及Icon类似的图片,不适合比较大型的图片。
(2)合并CSS和Javascript代码,开发过程中一般由多人合作开发,代码也按照一定的功能模块进行划分编写,所以开发完成无论是CSS还是Javascript都是多个代码文件,如果不进行合并,则多个文件会多次请求并下载,所以可以通过一些工程化的工具在代码发布的前夕将所有的代码进行合并,从而达到减少请求的目的。
(3)对于一些图片可以使用map的方法,这样一块区域只需要一次图片的请求,或者是把一些图片通过base64的编码方式内敛在页面中,不过这种方法可能会消耗一些CPU的资源。
(4)对于一些ajax请求,可以使用一些ajax框架中的方法,通过跟后台的配合,将多个ajax请求合并在一起,比如使用DWR来发送请求时就可以做到这点。

###1.2 服务端的优化
Combo技术最初出现源于《高性能网站建设指南》的规则一所提到“减少HTTP请求”,是一个在服务端提供,合并多个文件请求在一个响应中的技术。在生产环境中,Combo功能有很多实现,例如Tengine、Combo Handler(Yahoo!开发的一个Apache模块)。它实现了开发人员简单方便地通过URL来合并JavaScript和CSS文件,从而大大减少文件请求数,一个在服务端提供,合并多个文件请求在一个响应中的技术。

##二、压缩内容
在对http请求优化完毕之后,我们可以做的就是将具体的一个请求看看是否可以将文件的大小进行优化,文件越大需要消耗在传输时间上面的消耗也越大,特别在移动端,对于用户的流量要十分的谨慎和小心,因为移动端的流量要比PC端的资费要贵的多。
(1)由于前端的HTML、CSS、Javascript语言的特性,这些代码可以进行压缩,比如换行这些的字符可以不用,通过一些工程化的工具完全可以做到这点,将上诉三者的代码量进行优化,压缩后的文件大小变化很大,可以优化一半以上。一些比较出名的压缩工具比如YUI Compressor、Google的Closure Compiler、uglifyjs等。
(2)压缩图片:对于一些高质量的图片一些文件都比较大,现在也有一些成熟的技术可以较大幅度的将图片的大小进行压缩,但是同时又不会对图片的质量有很大的改变,比如可以在PS中设置图片的质量,从而降低文件的大小,或者是可以通过一些第三方的工具比如Smush.it,他 是 YUI 团队制作的一款基于 YUI 的在线图片优化工具,或者采用谷歌的一个新的图片格式WebP,不过他的支持度还不是很高,所以也需要谨慎使用。
(3)对于服务端可以使用Gzip压缩技术。传输的内容进行gzip压缩之后可以很明显的看到文件压缩的量,大大减少减少传输时间。

##三、缓存
对于一个Web页面中包含了很多组件资源,每次加载都会需要去下载这些资源,在移动端,这无论对于性能还是加载速度或者是流量都是一个不好的情况。而现在的大部分最新的浏览器支持一些缓存的方法,可以通过一系列的方法,将页面的资源缓存浏览器中,当用户再次访问的时候,可以直接调取浏览器中已有的内容,而不用重新去服务器下载资源,其实这样也就是为了减少http的请求,从而来达到优化的目的,具体的也可以分为在服务端和在前端的方法。

###3.1 前端的优化
按照正常的方案,为了减少http的请求,可以把CSS还有Javascript全部内连载页面内部,但是这样会让首屏的页面文件变大,虽然是减少了http的请求,但是会极大的影响首屏用户的体验,所以可以将Javascript和CSS改成外联的方式进行加载。这样在实际的使用中却会有着更快的加载速度,原因是现在的浏览器中大多数都是有着缓存的功能,所以这部分的文件都会被浏览器所缓存下来,这样在下次的加载有些明显的提升,不但不会增加HTTP请求数量,而且文档的体积也会减少。此外还可以利用浏览中存储功能比如Local Storage或者是manifest离线存储等。

###3.2 服务端的优化
(1)HTTP头信息Expires(过期时间) 属性是HTTP控制缓存的基本手段,这个属性告诉缓存器:相关副本在多长时间内是新鲜的。过了这个时间,缓存器就会向源服务器发送请求,检查文档是否被修改。几乎所有的缓存服务器都支持Expires(过期时间)属性;HTTP规范中简要的称该头为“在这一日期/时间之后,响应将被认为是无效的”。例如: Expires: Thu, 15 Apr 2010 20:00:00 GMT 告诉浏览器该响应的有效性持续到2010年4月15日。因为Expires头使用一个特定的时间,它要求服务端和客户端的时钟严格同步,但实际的情况下,服务端和客户端的时间经常不同步,而且时差比较严重,所以用这个方法也不是万能的。另外,过期日期需要经常检查,一旦过期日期到了,需要在服务器中配置提供一个新的日期。这类缓存比较适合那些网站中整体的图片背景,换句话说就是那些不太容易更换的地方,可以设置一个比较合适的时间,利用好缓存。
(2)Cache-Control的方法:由于Expires有着一些不方便的地方,所以HTTP1.1引入了Cache-Control头来克服Exipres头的限制。Cache-Control使用max-age指令指定组件被缓存多久,它以秒为单位定义了一个更新窗,这样就可以避免因为服务端和客户端之间的时间点不一样而产生的问题。使用带有max-age的Cache-Control可以消除Expires的限制,但对于不支持HTTP1.1的应用,仍希望使用Expires头。可以同时制定这两个响应头,如果两者同时出现时,HTTP规范规定max-age指令将重写Expires头。当出现了Expires头时,直到过期时间为止一直会使用缓存的版本,浏览器不会检查任何更新,直到过了过期时间。为了确保用户能够获取组件的最新版本,需要在所有的HTML页面中修改组件的文件名。
(3)If-Modified-Since是标准的HTTP请求头标签,所有现代的浏览器都支持最近修改 (last-modified) 的数据检查,在发送HTTP请求时,把浏览器端缓存页面的最后修改时间一起发到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行比较。如果时间一致,那么返回HTTP状态码304(不返回文件内容),客户端接到之后,就直接把本地缓存文件显示到浏览器中。如果时间不一致,就返回HTTP状态码200和新的文件内容,客户端接到之后,会丢弃旧文件,把新文件缓存起来,并显示到浏览器中,如果你曾经访问过某页,一天后重新访问相同的页时发现它没有变化,并奇怪第二次访问时页面加载得如此之快。
(4)ETag是Web服务器和浏览器用于确认缓存组件的有效性的一种机制。ETag在HTTP1.1中引入,用于检测浏览器缓存中的组件与原始服务器上的组件是否匹配。ETag是唯一标识了一个组件的一个特定版本字符串。唯一的约束是该字符串必须用引号引起来。原始服务器使用Etag响应头来指定组件的ETag。其工作方式是:服务器发送你所请求的数据的同时,发送某种数据的 hash (在 ETag 头信息中给出)。hash 的确定完全取决于服务器。当第二次请求相同的数据时,你需要在 If-None-Match: 头信息中包含 ETag hash,如果数据没有改变,服务器将返回 304 状态代码。与最近修改数据检查相同,服务器仅仅 发送 304 状态代码;第二次将不为你发送相同的数据。在第二次请求时,通过包含 ETag hash,你告诉服务器:如果 hash 仍旧匹配就没有必要重新发送相同的数据,因为你还有上一次访问过的数据。

##四、代码的优化

###4.1 HTML的优化
(1)为了让页面有着更好的用户体验,开发者应该尽量让首屏的显示尽可能的快,这里就需要尽量让首页的HTML页面代码尽可能的小,这里就需要涉及到评估是否将CSS或则是Javascript代码内敛到页面中,虽然确实会减少Http的请求数量,但是极大的增加页面文件的体积,可能会对首屏展示有一定的影响,具体需要开发者去平衡。同时为了更快的将首屏的内容展示出来,开发者应结构化HTML,尽量做到主要的内容先展示出来,次要的后续缓慢加载。同时拟写页面的结构的时候,尽量做到简洁,因为如果过多的DOM节点会影响页面的渲染速度,换言之以更少的代码实现同样的功能为最佳。
(2)在CSS写在文档的头部,这样可以让首屏可以尽快的有一定的样式;而Javascript则要尽可能的放在文档的尾部或者将其设置为异步加载,因为一般加载执行Javascript代码都会阻塞页面的加载和渲染。还有比如注意一些CSS的写法减少浏览器重绘回流等等。

###4.2 CSS的优化
(1)避免CSS表达式,之所以避免少使用CSS表达式的原因在雅虎YUI中也提到了“表达式的问题就在于它的计算频率要比我们想象的多。不仅仅是在页面显示和缩放时,就是在页面滚动、乃至移动鼠标时都会要重新计算一次。给CSS表达式增加一个计数器可以跟踪表达式的计算频率。在页面中随便移动鼠标都可以轻松达到10000次以上的计算量。”如此频繁的计算对性能来说肯定是一个很大的消耗。当然如果没有办法非要使用,那么也要尽可能的将其优化,CSS表达式方法在其它浏览器中不起作用,因此在跨浏览器的设计中单独针对Internet Explorer设置时会比较有用,或者对低版本中进行hack来避开。
(2)避免使用@import:页面中加载CSS文件的方法有两种,一种是link元素,另一种是CSS 2.1加入@import。而在外部的CSS文件中使用@import会使得页面在加载时增加额外的延迟。虽然规则允许在样式中调用@import来导入其它的CSS,但浏览器不能并行下载样式,就会导致页面增添了额外的往返耗时。比如,第一个CSS文件first.css包含了以下内容:@import url(“second.css”)。那么浏览器就必须先把first.css下载、解析和执行后,才发现及处理第二个文件second.css。当你在一个外部样式表中使用第二种方式时,浏览器无法通过并行下载的方式下载这个资源,这样就会导致其他资源的下载被阻塞。
(3)注意CSS选择器的性能:之所以要注意CSS选择器的性能是因为要注意浏览器匹配选择器和文档元素时所消耗的时间。这里有一种跟平时思维不同的就是有关CSS的解析顺序,平时在写CSS的时候,开发者都是习惯性的从左往右的顺序拟写CSS,但是实际上浏览器解析CSS的顺序却是相反的,是从右往左的顺序,这就会造成一个很大的区别,比如#header > a {font-weight:blod;},如果按照以前的思维方式,大家都会以为先查找到id为header的节点,再去子节点中寻找标签为a的节点,CSS这样查找的话效率还是挺高的;但是实际上却不是这样的,浏览器会去寻找所有的a节点,然后再找到父节点id为header的节点未知,这样的查找性能要比之前的差一些了。如果用#header a {font-weight:blod;}这样的方式去查找,性能要更加差,因为先要找到所有a标签中祖先元素id为header的元素直至根节点。

###4.3 Javascript的优化
(1)做好缓存:类似缓存资源一样,最常见的就是在for循环中,需要把数据的长度存储在一个临时变量中,否则每次循环都需要去读取这个数值;类似的还有在使用jQuery中,对于一个后续可能会继续用的节点,在一次$(“*”)取出后,最好将他缓存起来,因为每次支持节点的查找需要一定的代价和消耗。
(2)控制好经常触发事件的频率:比如有时候需要的时候会对浏览的onresize事件进行监听,如果不控制好频率,只是一味的在当外界resize的时候就触发这个动作,那么对于浏览的监听事件也有点过于损耗了,想象一下,当你调整浏览器的时候,他是每一个时间的间隔其实都触发了这个事件,特别是当那个监听事件里写了很多复杂的而且消耗资源的事件,这样貌似在低版本的一些浏览器都会卡住,解决的方法就是设置一个定时器,正常情况下延迟一定的时间执行,如果再次触发则取消之前的执行,大致的代码如下
var resizeEvent = null;
if (resizeEvent) {
clearTimeout(resizeEvent)
}
resizeEvent = setTimeout(function(){
/
event*/
}, 400);
(3)做好回收工作:由于Javascript语言的特性,它本身就具有垃圾回收的机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。特别在一些移动端的单页面中要特别注意垃圾回收的问题,因为不像PC端的一样一样会进场刷新,单页面经常仅仅利用ajax来更新数据,所以若不好好维护好内存的大小,很容易造成奔溃等情况出现。垃圾回收原理:找出内存中不再继续使用的变量,将其所占用的内存释放,开发人员不再关心内存的使用情况,内存的分配已经无用内存的释放完全实现了自动化管理。常见的Javascript垃圾回收分为两类:标记清除、引用计数。
标记清除是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间,IE,Firefox,Opera,Chrome和Safari的目前都是使用的标记清除回收策略。
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
在具体的实践中,开发者需要注意一些垃圾的回收,常见的比如,将DOM树中的一些节点删除时要一起把绑定在他上面的一些事件也同时给删除掉,这里不仅仅是为了清除不必要的事件有时甚至会出现一个动作执行多次的动作。还有就是比如闭包中的引用以及类似setTimeout这类定时器的设置,在不需要的情况下要及时清除。

##五、其他优化
(1)对一些不必要的资源可以采用惰性加载的方法来实现,即不使用的时候不加载,这样可以较大的优化页面的加载速度,比较常见的就是对于一些图片较多的页面上,可以首先加载仅在第一屏上的图片,对于页面中的不再第一屏的页面可以采用一个默认的图片,当且仅当用户下滑到那些图片的时候,再将他们从服务器加载进来。
(2)在兼容性允许的情况下尽量使用CSS动画而不是Javascript动画,因为使用CSS动画不占用Javascript的主线程,在一些手机上还可以利用GPU进行硬件加速(比如CSS3中的transitions属性等),很多浏览器还可以对这些动画进行优化。
(3)使用CDN(Content Delivery Network内容分发网络):其作用是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。最基本的开发者可以将自身常用的一些类库部署到CDN上,或者选择一些免费的CDN提供商来引入资源,常见的一些比如360、百度、七牛、又拍云等等。
(4)分多个域并行加载:在HTTP 1.1规范(section8.1.4)指明浏览器应允许每个主机名(hostname)可以支持至少两个并发连接,但是浏览器默认对同一个域下的资源同时加载的数量会有所限制,所以当一个文档中所包含的资源数要比主机的最大并发数还多的时候,浏览器则发出允许的最大并发数的请求,并将剩余的请求加入队列中。一旦有请求完成,浏览器会立即发出队列中的下一批允许的最大并发数的请求,它会一直重复这个过程直到下载完所有的资源,具体一点就是如果一个浏览器允许每个主机名可以有4个并发连接,并且一个页面可以引用同域的100个资源,那么每4个资源会占用1个往返时间,总的下载时间为25个往返时间。这个数量还跟浏览器的类型、版本、以及HTTP版本有关,根据最新的Browserscope的结果,分别如下:Chrome32、34为6个,Firefox26、27为6个,Safari7.0.1为6个,Blackberry为7个,Andriod 2.3为8个,IE 10为8个,IE11为13个。

但是浏览器同时还提供了另一种途径就是不同域下可以并行加载资源, 这样就可以绕开上面提到的限制问题,同样的图片如果将其放在两个域,不过虽然可以通过这个方法来达到更多资源的并行下载,但是也有两个问题,一个是所有的浏览器根据不同的种类对不同域同时下载的资源数有限制,根据最新的Browserscope的结果,分别如下:Chrome32、34为10个,Firefox26、27为17个,Safari7.0.1为17个,Blackberry为9个,Andriod 2.3为10个,IE 10为17个,IE11为17个。第二个问题是每一个域的解析需要时间,每个新的TCP的连接的建立引入额外的请求往返时间,同时还可能会造成空缓存客户的DNS查找,所以这个不同域的数量也不是越多越好,一般选取在2到5个之间,具体的可以按照不同的项目进行修改。
(5)DNS的预加载:当我们访问一个网站如 www.amazon.com 时,需要将这个域名先转化为对应的 IP 地址,这是一个非常耗时的过程,所以解析DNS是网站性能优化的比较重要的一部分,虽然加载时间不太长,但是很难压缩起来。特别是为了并发下载资源而使用多个CDN域名来加载资源的大型网站,更不可忽视,每加载资源之前都要先进行CDN域名的DNS解析转换。如果采用DNS预加载,常用的方法是可以在头部加上< link href=”//my.domain.com” rel=”dns-prefetch” />,支持该功能的浏览器就会提前对该域名进行DNS解析并且缓存一下,而不会在需要请求资源再进行解析,从而提高整体网站的性能。
(6)加一些过渡动画(loading状态):当访问一些含有众多多媒体元素的页面的时候,要是直接展现给用户一片空白的页面,这样的用户体验相当不好,一种常见的方法就是在页面加载完毕的时候,默认给一个loading状态的图片或者用CSS3来实现,等待内部的内容完全加载成功后再将这层loading状态的遮罩去掉。