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