年前,读完了《高性能站点建设指南》,可是一直没有整理。年后回来和同事一起出了份前端面试题,涉及到了关于性能优化的问题,在此特梳理一下。
大量的公司在开发功能业务时,仅仅关注功能点的实现,对于性能方面要求非常低甚至不作为考虑范围。在遇到一些性能瓶颈时,也往往通过加机器的暴力方式去减缓,但那并非解决这个问题的根本。作为前端project师。大部分人为了迎合需求一直在学习JavaScript、CSS、HTML5及Node。非常少去关注性能方面的东西。然而,有些性能的优化点仅仅须要花费非常少的时间和精力就能换来巨大的改善用户体验。
在陈述前端性能优化的问题之前,我们先思考例如以下问题:
一个页面从输入 URL 到页面载入显示完毕。这个过程中都发生了什么?
- 在浏览器输入地址。
- 浏览器查找域名的
IP
地址,包括DNS
详细的查找过程,包括:浏览器缓存、系统缓存、路由器缓存等; - 浏览器向
web
server发送一个HTTP
请求; - server的永久重定向响应(从
http://example.com
到http://www.example.com
); - 浏览器跟踪重定向地址;
- server处理请求;
- server返回一个
HTTP
响应; - 浏览器渲染显示
HTML
; - 浏览器发送请求获取嵌入在
HTML
中的资源(如图片、音频、视频、CSS
、JS
等等); - 浏览器发送异步请求。
參考地址:http://igoro.com/archive/what-really-happens-when-you-navigate-to-a-url/
性能黄金法则:仅仅有10%-20%的终于用户响应时间花在了下载HTML文档上。其余的80%-90%时间花在了下载页面中的全部组件上。
写在前面
在阐述优化点之前。有必要的先说明一下HTTP。其作为浏览器和server之间的一种传输协议。在整个过程中的作用至关重要。对HTTP很多其它的了解,推荐阅读《HTTP权威指南》。
压缩
压缩响应是最卓有成效的优化方式,浏览器能够使用Accept-Encoding头来声明支持压。server使用Content-Encoding头确认响应已被压缩。
==Request Headers==
Accept-Encoding:gzip
==Response Headers==
Content-Encoding:gzip
条件GET请求
假设浏览器在其缓存中保留了组件的一个副本。但并不确定它是否仍然有效。就会生成一个条件Get请求。假设确认缓存在副本仍然有效,浏览器就能够使用缓存中的副本。
典型情况下,缓存副本的有效性源自其最后改动时间。基于响应中的Last-Modified头,浏览器能够知道组件最后的改动时间。
它会使用If-Modified-Since头将最后改动时间发送给server。
假设组件没有被改动过。server会返回一个“304 Not Modified”状态码并不再发送响应体。从而得到一个更小且更快的相应。
在HTTP1.1中能够使用ETag和If-None-Match进行条件GET请求(以下讲述)。
==Request Headers==
If-Modified-Since:Thu, 07 Apr 2016 08:30:15 GMT
==Response Headers==
Last-Modified:Thu, 07 Apr 2016 08:30:15 GMT
Expires
条件GET请求和304响应有助于让页面载入得更快,但仍须要在client和server之间进行一次往返确认。以运行有效性检查。Expires头明白指出浏览器能否够使用组件的缓存副本。假设组件没有过期,浏览器就会使用缓存版本号而不会进行不论什么HTTP请求。
==Response Headers==
Expires:Thu, 07 Apr 2019 08:30:15 GMT
Keep-Alive
HTTP构建在传输控制协议TCP(Transmission Control Protocol)之上。HTTP早期实现中。每一个HTTP请求都要打开一个socket连接。
持久连接能够确保在单独的连接上进行多个请求。浏览器和server使用Connection头来指明对Keep-Alive的支持。
在HTTP1.1中并非必须的,HTTP1.1中定义的管道能够在一个单独的socket上发送多个请求,管道性能优于持久连接。
但IE7不支持,所以非常多浏览器和server仍然包括Keep-Alive。
==Request Headers==
Connection:keep-alive
==Response Headers==
Connection:keep-alive
规则1:降低HTTP请求
性能黄金法则中提到80%~90%时间花在HTML文档中组件下载。因此。降低组件的数量,并由此降低HTTP请求的数量。
是改善响应时间的最简单途径。
图片地图
对于“图片超链接”的情况,能够使用图片地图降低页面图片个数,从而降低HTTP请求。
其分为server端图片地图和client图片地图。详见:HTML5-嵌入内容
CSS Sprites
同图片地图,CSS Sprites也可合并图片。将多个图片合并到一个单独的图片中,使用CSS的background-position属性将HTML元素放到背景图片中期望的位置上。
<div style="background-image: url('sprites.git');
background-position: -260px -90px;
26px; height: 26px;">
</div>
注意:图片地图中的图片必须是连续的。而CSS Sprites则没有这个限制。
内敛图片
通过使用data: [<mediatype>][;base64],<data>
模式能够在Web页面中包括图片。而无需额外的HTTP请求(IE不支持)。要注意,在跨页面时不会被缓存。
不要去内联公司的logo,由于编码过的logo会导致页面变大。
聪明的做法是:使用CSS将内联图片作为背景。将其放在外部样式表中,数据能够缓存在样式表内部。尽管将内联图片放置在外部样式表中添加了一个额外的HTTP请求(请求样式表),但被缓存后能够得到额外的收获。
当然,对于仅仅使用一次(如,验证码)直接能够写在页面上。
演示样例:存放到样式表
.cart {background-img: url(data:image/png;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDA...)}
演示样例:直接页面嵌入
<img src="data:image/png;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDA..." />
合并脚本和样式表
合并脚本和样式表,是最普通只是的性能优化方式。能够使用Grunt、Webpack、Gulp等工具。这里不再赘述。须要思考的是:一个多页面的站点会有大量的模块,而模块的组合情况复杂,怎样合并模块值得花时间去分析一下自己的页面。确保组合的数量是可管理的。
规则2:使用内容公布网路
内容公布网路(CDN)是一组分布在多个不同地理位置的Webserver,用于更加有效地向用户公布内容,其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况。提高用户訪问站点的响应速度。使用CDN。须要注意:更新内容后,CDN的生效时间!
规则3:加入Expires头
Expires头在前面已经阐述过,其目的主要是最大化利用浏览器的缓存来改善页面的性能。
Expires头
浏览器(和代理)使用缓存来降低HTTP请求的数量,并降低HTTP响应的大小。
==Response Headers==
Expires:Thu, 07 Apr 2019 08:30:15 GMT
其告诉浏览器能够使用该组件的缓存。直到2019年4月7日上午8时30分15秒。
Max-Age和mod_expires
HTTP1.1中引入了Cache-Control
来克服Expires
头的限制。
Expires
头使用一个特定的时间。它要求server和client的时间严格同步(当然,能够通过Apache mode_expires模块中的ExpiresDefault以相对方式设置日期);另外,过期日期须要经常检查。一旦到达过去日期还须要在server端配置中提供一个新的日期。
Cache-Control: max-age=秒数
指定组件缓存多久(下述,缓存1年)。
==Response Headers==
Cache-Control: max-age=31536000
max-age
能够消除Expires的限制,但对于不支持HTTP1.1的浏览器。能够同一时候设置二者。二者同一时候存在时,HTTP规定max-age指令将重写Expires头。
修订文件名称
假设我们将组件配置能够在浏览器端进行缓存,当这些组件改变时用户怎样获得更新呢?设置了Expires头时。过期前会一直使用缓存版本号(从硬盘上读取组件),浏览器不会更新。为了确保用户能够获取组件的最新版本号,须要在全部HTML页面改动组件的文件名称。经常使用方式是添加MD5戳。
规则4:配置ETag
实体标签(Entity Tag。ETag)是Webserver和浏览器用于确认缓存组件有效性的一种机制。
浏览器下载组件后,会进行缓存。再次使用该组件时,会根据Expires头的值,推断是否发起请求。假设过期了。浏览器在重用之前必须检查他是否仍然有效,发送条件GET请求(前面已经提及)。假设是有效的。server会返回“304 Not Modified”,不会返回整个组件;这比简单地下载全部过期的组件效率要高。
server在检測缓存组件是否和原始server上的组件匹配时有两种方式:
- 比較最新改动日期 (If-Modified-Since ==> Last-Modified)
- 比較实体标签 ()
最新改动日期
原始server通过Last-Modified响应头来返回组件最新改动日期。
==Request Headers==
GET /1/4/A/1_ligang2585116.jpg
Host: avatar.csdn.net
==Response Headers==
HTTP 1.1 200 OK
Last-Modified: Wed, 11 Nov 2015 20:24:15 GMT
Content-Length: 19613
下一次请求http://avatar.csdn.net/1/4/A/1_ligang2585116.jpg时,浏览器会使用If-Modified-Since头将最新改动日期传回到原始server以进行比較。假设server上组件的最新改动日期与浏览器传回的值匹配,返回304,不会传送19613字节的数据。
==Request Headers==
GET /1/4/A/1_ligang2585116.jpg
Host: avatar.csdn.net
If-Modified-Since: Wed, 11 Nov 2015 20:24:15 GMT
==Response Headers==
HTTP 1.1 304 Not Modified
比較实体标签
ETag在HTTP1.1中引入,ETag是唯一标识了一个组件的一个特定版本号的字符串。
==Request Headers==
GET /1/4/A/1_ligang2585116.jpg
Host: avatar.csdn.net
==Response Headers==
HTTP 1.1 200 OK
Last-Modified: Wed, 11 Nov 2015 20:24:15 GMT
ETag: "8224274EB79860E83F60346E0EEBE99A"
Content-Length: 19613
ETag的加入为验证实体提供了比較新改动日期更灵活的机制。
比如,假设实体根据User-Agent或Accept-Language头而改变。实体的状态能够反映在ETag中。
浏览器会使用If-None-Match头将ETag传回原始server以进行比較。假设server上组件的ETag值与浏览器传回的值匹配,返回304,不会传送19613字节的数据。
==Request Headers==
GET /1/4/A/1_ligang2585116.jpg
Host: avatar.csdn.net
If-Modified-Since: Wed, 11 Nov 2015 20:24:15 GMT
If-None-Match: "8224274EB79860E83F60346E0EEBE99A"
==Response Headers==
HTTP 1.1 304 Not Modified
ETag带来的问题
ETag通常使用组件的某些属性构造而成,这些属性相应特定的、寄宿了站点server来说是唯一的。当浏览器从一台server上获取了原始组件。之后。又向另外一台不同的server发送提交GET请求,ETag是不会匹配的–这对于server集群来处理请求的站点非经常见,大大降低有效验证的成功率。
If-None-Match比If-Modified-Since具有更高的优先级。你可能希望假设ETag不匹配但最新改动时间同样,也能发送一个“304 Not Modified”响应,但实际并非这种。
HTTP1.1规范https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4,假设请求中同一时候出现了这两个头,则原始server“禁止(MUST NOT)”返回304。除非请求中的条件头字段全部一致。
ETag—用还是不用
上面描写叙述了ETag对于集群式站点的严重问题。但你可能会说,利用长久Expires头。使组件更长时间缓存到client。可是一旦用户点击了Reload或Refreshbutton。依旧会产生条件GET请求。
所以,你能够定义ETag仅仅保留大小和时间戳作为内容,或者直接移除ETag。