前端资源很多,包括HTML、CSS、JavaScript、Image、Flash等资源。前端优化是复杂的,有很多细分的优化方式,很多人都是大致了解一部分,但具体怎么优化却好无头绪。
首先,我们先要弄明白,前端优化的目的是什么?
1、从用户角度来说,优化可以让页面加载速度更快,对用户的操作响应更及时,减少等待时间,提升用户体验。
2、从服务商角度来说,优化能够减少页面请求数、或减小请求所占带宽,能够节省可观的资源。
前端优化的途径按粒度可大致分为两类。
一、页面级优化
1、减少HTTP请求数
2、避免重复的资源请求
3、减少cookie传输
4、Lazy Load
5、将CSS放在HEAD中
6、将外部脚本置底
7、减少重定向
8、异步请求Callback
9、使用CDN
10、使用反向代理
二、代码级优化
1、减少DOM操作
2、慎用with
3、避免使用eval和Function
4、减少作用域链查找
5、数据访问
6、字符串拼接
7、CSS选择符优化
如果你的页面像百度首页一样简单,那么接下来的规则基本上都用不着了。保持页面简洁、减少资源的使用时最直接的。如果不是这样,你的页面需要华丽的皮肤,则继续阅读下面的内容。
适当的缓存设置可以大大减少HTTP请求。(这里需要说明的是,如果直接 F5刷新页面的话效果是不一样的,这种情况下请求数还是一样,不过被缓存资源的请求服务器是 304响应,只有 Header没有Body ,可以节省带宽)
设置原则很简单:能缓存越多越好,能缓存越久越好。例如,很少变化的图片资源可以直接通过 HTTP Header中的Expires设置一个很长的过期头 ;变化不频繁而又可能会变的资源可以使用 Last-Modifed来做请求验证。尽可能的让资源能够在缓存中待得更久。
!!!此处着重了解HTTP缓存的具体设置和原理!!!
现在提供了很多打包工具可以压缩CSS、JavaScript、Image,压缩后往往能省下不少空间。打包工具有webpack、gulp、grunt…
合并CSS图片,可以减少请求数。可使用雪碧图,把所有相对较小的资源图片,绘制在一张大图上,只需要将大图下载下来,然后利用图片定位来讲小图展现在页面中(background-position:百分比,数值)
使用data:URL scheme的方式将图片嵌入到页面或 CSS中,如果不考虑资源管理上的问题的话,不失为一个好办法。如果是嵌入页面的话换来的是增大了页面的体积,而且无法利用浏览器缓存。使用在 CSS中的图片则更为理想一些。
在团队开发一个项目时,由于不同开发者之间都可能会向页面中添加页面或组件,因此可能相同的脚本会被添加多次。
重复的脚本会造成不必要的HTTP请求(如果没有缓存该脚本的话),并且执行多余的JavaScript浪费时间,还有可能造成错误。
(1)形成良好的脚本组织。重复脚本有可能出现在不同的脚本包含同一段脚本的情况,有些是必要的,但有些却不是必要的,所以需要对脚本进行一个良好的组织。
(2)实现脚本管理器模块。
一方面,cookie包含在每次请求和响应中,太大的cookie会严重影响数据传输,因此哪些数据需要写入cookie需要慎重考虑,尽量减少cookie中传输的数据量。另一方面,对于某些静态资源的访问,如CSS、script等,发送cookie没有意义,可以考虑静态资源使用独立域名访问,避免请求静态资源时发送cookie,减少cookie传输次数。
这条策略实际上并不一定能减少 HTTP请求数,但是却能在某些条件下或者页面刚加载时减少 HTTP请求数。对于图片而言,在页面刚加载的时候可以只加载第一屏,当用户继续往后滚屏的时候才加载后续的图片。这样一来,假如用户只对第一屏的内容感兴趣时,那剩余的图片请求就都节省了。
将样式表放在头部对于实际页面加载的时间并不能造成太大影响,但是这会减少页面首屏出现的时间,使页面内容逐步呈现,改善用户体验,防止“白屏”。
js的下载和执行会阻塞Dom树的构建(严谨地说是中断了Dom树的更新),也会会阻塞其他资源,例如在脚本加载完成之前,它后面的图片、样式以及其他脚本都处于阻塞状态,直到脚本加载完成后才会开始加载。所以script标签放在首屏范围内的HTML代码段里会截断首屏的内容。
重定向用于将用户从一个URL重新路由到另一个URL。
301:永久重定向,主要用于当网站的域名发生变更之后,告诉搜索引擎域名已经变更了,应该把旧域名的的数据和链接数转移到新域名下,从而不会让网站的排名因域名变更而受到影响。
302:临时重定向,主要实现post请求后告知浏览器转移到新的URL。
304:Not Modified,主要用于当浏览器在其缓存中保留了组件的一个副本,同时组件已经过期了,这是浏览器就会生成一个条件GET请求,如果服务器的组件并没有修改过,则会返回304状态码,同时不携带主体,告知浏览器可以重用这个副本,减少响应大小。
当页面发生了重定向,就会延迟整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现任何东西,也没有任何组件会被下载。
来看一个实际例子:对于ASP.NET webform开发来说,对于新手很容易犯一个错误,就是把页面的连接写成服务器控件后台代码里,例如用一个Button控件,在它的后台click事件中写上:Response.Redirect("");然而这个Button的作用只是转移URL,这是非常低效的做法,因为点击Button后,先发送一个Post请求给服务器,服务器处理Response.Redirect("")后就发送一个302响应给浏览器,浏览器再根据响应的URL发送GET请求。正确的做法应该是在html页面直接使用a标签做链接,这样就避免了多余的post和重定向。
重定向经常用于跟踪用户流量的方向,当拥有一个门户主页的时候,同时想对用户离开主页后的流量进行跟踪,这时可以使用重定向。例如: 某网站主页新闻的链接地址http://a.com/r/news,点击该链接将产生301响应,其Location被设置为http://news.a.com。通过分析a.com的web服务器日志可以得知人们离开首页之后的去向。
我们知道重定向是如何损伤性能的,为了实现更好的效率,可以使用Referer日志来跟踪内部流量去向。每个HTTP请求都有一个Referer表示原始请求页(除了从书签打开或直接键入URL等操作),记录下每个请求的Referer,就避免了向用户发送重定向,从而改善了响应时间。
有时链接可能将用户带离你的网站,在这种情况下,使用Referer就不太现实了。
同样也可以使用重定向来解决跟踪出站流量问题。
除了重定向外,我们还可以选择使用信标(beacon)——一个HTTP请求,其URL中包含有跟踪信息。跟踪信息可以从信标Web服务器的访问日记中提取出来,信标通常是一个1px*1px的透明图片,不过204响应更优秀,因为它更小,从来不被缓存,而且绝不会改变浏览器的状态。
在某些页面中可能存在这样一种需求,需要使用 script标签来异步的请求数据。类似:
像以上这种方式直接在页面上写 script 对页面的性能也是有影响的,由于浏览器在页面处理方面是单线程的,当 inline脚本在页面渲染之前执行时,页面的渲染工作则会被推迟,即增加了页面首次加载的负担,推迟了 DOMLoaded和window.onload 事件的触发时机。简而言之, inline脚本在执行的时候,页面处于空白状态。鉴于以上两点原因,如果时效性允许的话,建议将执行时间较长的inline脚本异步执行,异步的方式有很多种,例如可以考虑在 DOMLoaded事件触发的时候加载,或者使用setTimeout方式来灵活的控制加载的时机,或者使用 script元素的defer 属性(存在兼容性问题和其他一些问题,例如不能使用 document.write)。此外,在HTML5中引入了 Web Workers的机制,恰恰可以解决此类问题。
CDN(内容发布网络)是一组分布在多个不同地理位置的Web服务器,用于更加有效地向用户发布内容。在优化性能时,向特定用户发布内容的服务器的选择基于对网络慕课拥堵的测量。例如,CDN可能选择网络阶跃数最小的服务器,或者具有最短响应时间的服务器。
CDN还可以进行数据备份、扩展存储能力,进行缓存,同时有助于缓和Web流量峰值压力。
如果应用程序web服务器离用户更近,那么一个HTTP请求的响应时间将缩短。另一方面,如果组件web服务器离用户更近,则多个HTTP请求的响应时间将缩短。
CDN的缺点:
1、响应时间可能会受到其他网站流量的影响。CDN服务提供商在其所有客户之间共享Web服务器组。
2、如果CDN服务质量下降了,那么你的工作质量也将下降
3、无法直接控制组件服务器
传统代理服务器位于浏览器一侧,代理浏览器将http请求发送到互联网上,而反向代理服务器位于网站机房一侧,代理网站web服务器接收http请求。如下图所示:(pic1.png)
论坛网站,把热门词条、帖子、博客缓存在反向代理服务器上加速用户访问速度,当这些动态内容有变化时,通过内部通知机制通知反向代理缓存失效,反向代理会重新加载最新的动态内容再次缓存起来。
此外,反向代理也可以实现负载均衡的功能,而通过负载均衡构建的应用集群可以提高系统总体处理能力,进而改善网站高并发情况下的性能。
a.HTML Collection(HTML收集器,返回的是一个数组内容信息)
在脚本中 document.images、document.forms、getElementsByTagName()返回的都是HTMLCollection类型的集合,在平时使用的时候大多将它作为数组来使用,因为它有 length属性,也可以使用索引访问每一个元素。不过在访问性能上则比数组要差很多,原因是这个集合并不是一个静态的结果,它表示的仅仅是一个特定的查询,每次访问该集合时都会重新执行这个查询从而更新查询结果。所谓的“访问集合” 包括读取集合的 length属性、访问集合中的元素。
因此,当你需要遍历 HTML Collection的时候,尽量将它转为数组后再访问,以提高性能。即使不转换为数组,也请尽可能少的访问它,例如在遍历的时候可以将 length属性、成员保存到局部变量后再使用局部变量。
除了上面一点之外, DOM操作还需要考虑浏览器的Reflow和Repaint ,因为这些都是需要消耗资源的。
with(obj){ p = 1}; 代码块的行为实际上是修改了代码块中的执行环境 ,将obj放在了其作用域链的最前端,在 with代码块中访问非局部变量是都是先从 obj上开始查找,如果没有再依次按作用域链向上查找,因此使用 with相当于增加了作用域链长度。而每次查找作用域链都是要消耗时间的,过长的作用域链会导致查找性能下降。
因此,除非你能肯定在 with代码中只访问 obj中的属性,否则慎用 with,替代的可以使用局部变量缓存需要访问的属性。
每次 eval 或Function 构造函数作用于字符串表示的源代码时,脚本引擎都需要将源代码转换成可执行代码。这是很消耗资源的操作 —— 通常比简单的函数调用慢 100倍以上。
eval 函数效率特别低,由于事先无法知晓传给 eval 的字符串中的内容,eval在其上下文中解释要处理的代码,也就是说编译器无法优化上下文,因此只能有浏览器在运行时解释代码。这对性能影响很大。
Function 构造函数比 eval略好,因为使用此代码不会影响周围代码 ;但其速度仍很慢。
此外,使用 eval和 Function也不利于Javascript 压缩工具执行压缩。
如果在循环中需要访问非本作用域下的变量时请在遍历之前用局部变量缓存该变量,并在遍历结束后再重写那个变量,这一点对全局变量尤其重要,因为全局变量处于作用域链的最顶端,访问时的查找次数是最多的。
低效率的写法:
更高效的写法:
此外,要减少作用域链查找还应该减少闭包的使用。
Javascript中的数据访问包括直接量 (字符串、正则表达式 )、变量、对象属性以及数组,其中对直接量和局部变量的访问是最快的,对对象属性以及数组的访问需要更大的开销。当出现以下情况时,建议将数据放入局部变量:
a. 对任何对象属性的访问超过 1次
b. 对任何数组成员的访问次数超过 1次
另外,还应当尽可能的减少对对象以及数组深度查找。
在 Javascript中使用”+”号来拼接字符串效率是比较低的,因为每次运行都会开辟新的内存并生成新的字符串变量,然后将拼接结果赋值给新变量。与之相比更为高效的做法是使用数组的 join方法,即将需要拼接的字符串放在数组中最后调用其 join方法得到结果。不过由于使用数组也有一定的开销,因此当需要拼接的字符串较多的时候可以考虑用此方法。
在大多数人的观念中,都觉得浏览器对 CSS选择符的解析式从左往右进行的,例如
#toc A { color: #444; }这样一个选择符,如果是从右往左解析则效率会很高,因为第一个 ID选择基本上就把查找的范围限定了,但实际上浏览器对选择符的解析是从右往左进行的。如上面的选择符,浏览器必须遍历查找每一个 A标签的祖先节点,效率并不像之前想象的那样高。根据浏览器的这一行为特点,在写选择符的时候需要注意很多事项,有兴趣的童鞋可以去了解一下。