高性能网站建设指南 读书笔记1
时间花在哪儿了
至少80%的最终用户的响应时间花在了页面的组件(图片、脚本、样式表、flash)中,只有10%-20%花费在下载html文档上,也没有做任何的后端处理,这就是为什么前端性能很重要。
在前端进行性能优化的优势
- 1、前端可以更好的提高整体性能,如果我们可以将后端响应时间缩短一半,整体响应时间只能减少5%-10%,而如果关注前端性能,整体响应时间可以减少40%-45%。
- 2、改进前端通常只需要较少的时间和资源,而改进后端花费的时间明显更长,代价越大。
- 3、前端性能调整已被证明是可行的。
响应时间
网站响应时间是指从请求初始化完毕到页面的onload
事件被触发所经过的时间。
http概述
http
是一种客户端/服务端协议,由请求和响应构成,浏览器向一个特定的url
发送http
请求,url
对应的宿主服务器返回http
响应,该协议使用简单的纯文本格式。
最常用的是GET
请求,请求包含一个url
,请求头。http
响应包含状态码,响应头,响应体。
// 请求头
GET /asset/js/app.js
HTTP 1.1
Host: localhost:8080
User-Agent: xxxx
// 响应头
HTTP 1.1 200 OK
Content-Type: application/x-javascript
Last-Modified: Wed, 22 Feb 2006 04:14:45 GMT
Content-Length: 355
...
如果浏览器和服务器支持,可以使用压缩来减小响应的大小,浏览器可以使用Accept-Encoding
头来声明它支持压缩,服务器使用Content-Encoding
头确认响应已被压缩。
// 请求头
...
Accept-Encoding: gzip,deflate
// 响应头
Content-Encoding: gzip
条件GET请求
如果浏览器在其缓存中保留了组件的一个副本,但并不确定他它是否仍然有效,就会生成一个条件GET
请求,如果确认缓存的副本有效,浏览器就可以使用缓存中的副本,这会得到更小的响应和更快的用户体验。(返回304)
典型情况下,缓存副本的有效性源自其最后修改时间,基于服务器响应头中的Last-Modified
,浏览器可以知道组件最后的修改时间。它会使用If-Modified-Since
请求头将最后修改时间发送给服务器,相当于是浏览器在询问服务器,“我拥有这个组件的一个版本,这是它最后修改的时间,我可以使用它吗”,如果组件没有改变过,服务器会响应'304 Not Modified'
状态码并不再发送响应体,从而得到一个更小且更快的响应,浏览器则直接使用缓存中的副本。
Expires
条件GET
请求和304
响应有助于让页面加载的更快,但仍需要在客户端和服务器之间进行一次往返确认,以执行有效检查,Expires
响应头通过明确指出浏览器是否可以使用组件的缓存副本来消除这个需要,减少一次请求。
// 响应头
HTTP 1.1 200 OK
Content-Type: application/x-javascript
Last-Modified: Wed, 22 Feb 2006 04:14:45 GMT
Expires: Wed, 05 Oct 2016 19:16:20 GMT
当浏览器看到响应中有一个Expires
头时,它会和相应的过期时间组件一起保存到其缓存中,只要组件没有过期,浏览器就会使用缓存版本而不会径向任何HTTP
请求。
Keep-Alive
HTTP
构建在TCP
之上,在HTTP
的早期实现中,每个HTTP
请求都要打开一个socket
连接,这样做效率很低,因为一个web
页面中的很多HTTP
请求都指向同一个服务器,持久连接的引入解决了多对一请求服务器导致的socke
连接低效性的问题,它使浏览器可以在一个单独的连接上进行多个请求,浏览器和服务器使用Connection
头来指出对Keep-Alive
的支持,并且写法是一样的。
// 请求头
...
Connection: keep-alive
// 响应头
Connection: keep-alive
浏览器或服务器可以通过发送一个 Connection: close
头来关闭连接,HTTP 1.1
中定义的管道可以在一个单独的socket
上发送多个请求而无需等待响应,但是在管道被广泛应用之前,Keep-Alive
依然是浏览器和服务器使用HTTP
的socket
连接最有效的方式,这对于HTTPS
来说甚至更重要,因为建立新的安全socket
连接要消耗更多的时间。
规则1 - 减少HTTP请求
CSS Sprites
使用CSS Sprites
是将多个图片合并到一个单独的图片中,可以使用在任何支持背景图片的HTML
元素上,使用CSS
中的background-position
属性,将HTML
元素放置到背景图片中期望的位置上。
div {
background-image: url('a.png');
background-position: -200px -90px;
width: 30px;
height: 30px;
}
CSS Sprites
通过background-position
属性调整来展示其中的某一个图标,而且它还降低了下载量,合并后的图片尽管会有附加的空白区域,但是实际却比分离的图片总和要小,因为它降低了图片自身的开销(颜色表,格式信息等等),如果要在页面中为背景,按钮,导航栏,链接等提供大量图片,CSS Sprites
绝对是一种优秀的解决方法——干净的标签,很少的图片和很短的响应时间。
内联图片
通过使用data: URL
模式(base64编码)可以在Web页面中包含图片但无需任何额外的HTTP
请求。数据就在其URL自身之中,格式如下:
// data:[<mediatype>][;base64],<data>
<img src="data:image/gif;base64,ROAOPAOAJMNxSNKLAvDASDaADLKASLDAa/lvrkxxxxxxxxxxxx="/>
data:
大多用于内联图片,但实际它可以用在任何需要指定URL
的地方,包括script
和a
标签。但缺陷是不支持IE7
及以下版本,并且存在数据大小上的限制,Base64
编码会增加图片的大小,因此整体下载量会增加。
由于data:URL
是内联在页面中的,在跨越不同页面时不会被缓存,聪明的做法是使用CSS
并将内联图片作为背景,将该CSS
规则放在外部样式表中,这意味着数据可以缓存在样式表内部。
div {
background-image: url(data:image/gif;base64,ROAOPAOAJMNxSNKLAvDASDaADLKASLDAa/lvrkxxxxxxxxxxxx=);
}
合并脚本和样式表
如果遵循模块化的原则,将代码分开放到多个小文件中会降低性能,因为每个文件都会导致一个额外的HTTP
请求,所以最终将这些单独的文件合并到一个文件中,可以减少HTTP
请求的数量来缩短响应时间。虽然将代码最后又合并在一起,看起来像是一种倒退,而且所有的JS
合并为一个单独的文件在开发环境中很难完成,一个页面可能需要script1
、script2
,而另一个页面需要script1
、script3
、script4
,解决的方法是遵守编译型语言的模式,保持JS的模块化,而在生成过程中从一组特定的模块生成一个目标文件。
小结
该规则(减少HTTP
请求)可以同时改善首次浏览和后续浏览的网站响应时间,而首次访问页面时的响应时间决定这用户是放弃你的网站还是不停的进行回访。
规则2 - 使用内容发布网络(cdn content delivery network)
网站最初通常将其所有的服务器放在同一个地方,当用户群增加的时候,公司就必须面对服务器放置地点不在适用的事实——有必要在多个地理位置不同的服务器上部署内容。
如果应用程序web服务器离用户更近,则一个HTTP
请求的响应时间将缩短,另一方面,如果组件web服务器离用户更近,则多个HTTP
请求的响应时间将缩短。
内容发布网络(CDN
)是一组分布在多个不同地理位置的web服务器,用于更加有效地向用户发布内容。不仅能提高性能,还能节省成本。在优化性能时,向特定用户发布内容的服务器的选择基于对网络可用度的测量。例如,CDN
可能选择网络阶跃数最小的服务器,或者具有最短响应时间的服务器。
不同CDN
服务提供商都以不同的方式部署CDN
,有些需要最终用户使用一个代理来配置他们的浏览器,有的需要开发者使用不同的域名修改他们的组件的URL
,无论如何也不要使用HTTP
重定向来将用户指向到本地服务器,这会使web页面反应速度变慢。
除了缩短响应时间外,CDN
还可以带来其他优势。他们的服务包括备份、扩展存储能力和进行缓存。CDN
还有助于缓和web流量峰值压力,如在获取天气或股市新闻、浏览流行的体育或娱乐时间时。
依赖CDN
的一个缺点是你的响应时间可能会受到其他网站——甚至很可能是你的竞争对手流量的影响。CDN
服务提供商在其所有客户之间共享web服务器组。另一个缺点是无法直接控制组件服务器所带来的特殊麻烦。例如,修改HTTP
响应头必须通过服务器提供商来完成,而不是你的工作团队来完成。最后,如果CDN
服务的性能下降了,你的工作质量也随之下降。
如果你以你自己的响应时间测试衡量使用CDN
的优势,千万记住你运行测试的地理位置对你的结果有着重要的影响,要测量切换到CDN
的真实影响就必须在多个地理位置上测量响应时间,与服务器之间的地理距离影响CDN
的响应时间。
规则3 - 添加Expires头
通过使用Expires
头,使其能够最大化利用浏览器的缓存能力来改善页面的性能。页面的初访者会进行很多HTTP
请求,但通过一个长久的Expires
头,可以使这些组件被缓存,这会在后续的页面浏览中避免不必要的HTTP
请求。长久的Expries
头最常用于图片,当然添加长久的Expires
头会带来额外的开发成本。
Expires
浏览器(和代理)使用缓存来减少HTTP
请求的数量,并减小HTTP
响应的大小,使web页面加载得更快。web服务器使用Expires
头来告诉客户端它可以使用一个组件的当前副本,直到指定的时间为止。
// 响应头
Expires: Mon, 15 Apr 2024 20:00:00 GMT
这是一个非常长久的Expires
头,它告诉浏览器该响应的有效性持续到2024年4月15日
为止,如果为页面中的一个图片返回了这个头,浏览器在后续的页面浏览中会使用缓存的图片,将HTTP
数量减少一个。
Max-Age
因为Expires
头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。另外,过期日期需要经常检查,并且一旦未来的这一天到来了,还需要在服务器配置中提供一个新的日期。
换一种方式,Cache-Control
使用max-age
指令指定组件被缓存多久,它以秒为单位定义了一个更新窗。如果从组件被请求开始过去的秒数少于max-age
,浏览器就使用缓存的版本,这就避免了额外的HTTP
请求,一个长久的max-age
头可以将刷新窗设置为未来10年。
// 响应头
Cache-Control: max-age=315360000
使用带有max-age
的Cache-Control
可以消除Expires
的限制,但对于不支持HTTP 1.1
的浏览器(极少部分),你可能仍然希望提供Expires
头,你可以同时制定者两个响应头————Expires
和Cache-Control max-age
。如果两者同时出现,HTTP
规范规定max-age
指令将重写Expires
头,同样的,你仍然需要担心Expires
带来的时钟同步问题和配置维护问题。
空缓存 vs 完整缓存
只有在用户已经访问过你的网站以后,长久的Expires
头才会对页面浏览产生影响,当用户第一次访问你的网站是,它不会对HTTP
请求的数量产生任何影响,此时浏览器的缓存是空的,因此,其性能的改进取决于用户在访问你的页面时是否有完整缓存,你的访问流量几乎都来自于那些有完整缓存的用户。使你的组件可缓存能够改善这些用户的响应时间。
空缓存或者完整缓存,指的是与你的页面相关的浏览器缓存状态,如果你的页面中的组件没有放在缓存中,则缓存为空。反之,如果你页面的可缓存组件都在缓存中,则缓存是完整的。通过使用长久的Expires
头可以增加被浏览器缓存的组件的数量,并在后续页面浏览中重用它们,而无需通过用户的Internet
连接发送一个字节。
不仅仅是图片
为图片使用长久的Expires
头非常之普遍,但这一最佳实践不应该仅限于图片。长久的Expires
头应该包含任何不经常变化的组件,包括脚本、样式表和 Flash组件。HTML
文档不应该使用长久的Expires
头,因为它包含动态的内容,这些内容在每次用户请求时都将被更新。
在理想情况下,页面中的所有组件都应该具有长久的Expires
头,并且后续的页面浏览中只需为HTML
文档进行一个HTTP
请求。当文档中的所有组件都是从浏览器缓存中读取出来时,响应时间会减少50%或更多。
如何更新缓存
如果我们将组件配置为可以由浏览器代理缓存,当这些组件改变时用户如何获得更新呢?当出现了Expires
头时,直到过期日期为止一直会使用缓存的版本。浏览器不会检查任何更新,直到过了过期时间,这也是为什么使用Expires
头能够显著减少响应时间————浏览器直接从硬盘上读取组件而无需生成任何HTTP
流量。因此,即使在服务器上更新了组件,已经访问过网站的用户也不大可能获取最新的组件(因为前一个版本已经在他们的缓存中了)
为了确保用户能够获取组件的最新版本,需要在所有HTML
页面中修改组件的文件名,最有效的解决方案是修改其所有链接,这样,全新的请求将从原始服务器下载最新的内容。
一种简单的解决方案就是为所有组件的文件名使用变量。使用这种方法,在页面更新文件名时只需要简单在某个地方修改变量,一般将这一步作为生成过程中的一部分,将版本号嵌入在组件的文件名中,而且在全局映射中修订过的文件名会自动更新。嵌入的版本后不仅可以改变文件名,还能在调试时更容易地找到准确的源代码文件。
如果一个组件没有长久的Expires
头,它仍然会存储在浏览器缓存中。在后续请求中,浏览器会检查缓存并发现组件已经过期。为了提高效率,浏览器会向原始服务器发送一个条件GET
请求,如果组件没有改变,原始服务器可以免于发送整个组件,而是发送一个很小的头,告诉浏览器可以使用其缓存的组件。
这些条件请求加起来,就是使用Expires
会节省的时间。很多时候,组件并没有改变,而浏览器总是从磁盘上读取他们。通过使用Expires
头避免额外的HTTP
请求,可以减少一半的响应时间。
规则4 - 压缩组件
压缩组件通过减小HTTP
响应的大小来减少响应时间,如果HTTP
请求产生的响应包很小,传输时间就会减少,因为只需要将很小的包从服务器传送到客户端,这一效果对速度较慢的带宽尤为明显,使用gzip
编码来压缩HTTP
响应包,减少网络响应时间。这是减小页面大小的最简单的技术,但是影响是最大的。还有很多方式可以减小HTML
文档的页面大小(删除注释和缩短URL等),但它们需要更多的工作,且收效甚微。
压缩是如何工作的
从HTTP 1.1
开始,web客户端可以通过HTTP
请求中的Accept-Encoding
头表示对压缩的支持。
// 请求头
Accept-Encoding: gzip, deflate
如果web服务器看到请求中有这个头,就会使用客户端列出来的方法中的一种来压缩响应,web服务器通过响应中的Content-Encoding
头来通知web客户端。
// 响应头
Content-Encoding: gzip
压缩什么
服务器基于文件类型选择压缩什么,但这通常受限于对其进行的配置,很多网站会压缩器HTML
文档,当然压缩脚本和样式表也是非常值得的。图片和PDF不应该压缩,因为他们本来就已经被压缩过了,试图对他们进行压缩只会浪费CPU
资源,还有可能会增加文件大小。
压缩的成本————服务器端会花费额外的CPU
周期来完成压缩,客户端要对压缩文件进行解压缩。要检测收益是否大于开销,需要考虑响应的大小、连接的带宽和客户端与服务器之间的Internet
距离,通常这些信息难以得到,即使得到了,也有很多其他变数需要考虑,根据经验通常对大于1KB
或2KB
的文件进行压缩。
压缩通常能将响应的数据量减少将近70%。
代理缓存
web服务器基于Accept-Encoding
来检测是否对响应进行压缩。不管是否压缩过,浏览都会基于响应中的其他HTTP
头如Expires
和Cache-Control
来缓存响应。
当浏览器通过代理来发送请求时,情况就变得复杂了。假设针对某个URL
发送到代理的第一个请求来自于不支持gzip
的浏览器。这是到达代理的第一个请求,因此其缓存为空。代理会将请求转发到web服务器。此时服务器的响应是未经过压缩的。这个没有压缩的响应会被代理缓存起来并发送给浏览器。现在,假设到达代理的第二个请求访问的是同一个URL
,来自于一个支持gzip
的浏览器。代理会使用缓存中(未经压缩)的内容进行响应。这就失去了进行压缩的机会。如果顺序反了,第一个请求来自于支持gzip
的浏览器,而第二个请求来自于不支持gzip
的浏览器,情况可能更加严重,代理缓存中拥有内容的一个压缩版本,并将这个版本提供给后续的浏览器,而不管它们是否支持gzip
。
解决这一问题的方法是在web服务器的响应中添加 Vary
头,web服务器可以告诉代理根据一个或多个请求头来改变缓存的响应。由于压缩的决定是基于Accept-Encoding
请求头的,因此需要在服务器的 Vary
响应头中包含Accept-Encoding
。
// 响应头
Vary: Accept-Encoding
这将使得代理缓存响应的多个版本,为Accept-Encoding
请求头的每个值缓存一份,在前面的例子中,代理会缓存每个响应的两个版本———— Accept-Encoding
为gzip
时的压缩内容 和 没有指定Accept-Encoding
时的非压缩版本。当浏览器带着Accept-Encoding: gzip
访问代理时,它接受到的就是压缩过的内容。没有Accept-Encoding
请求头的浏览器收到的就是未经压缩过的内容,一般都会为所有响应添加Vary: Accept-Encoding
头。
边缘情形
服务器和客户端的压缩对等性看似简单,但必须正确才行。无论是客户端还是服务器发送错误(发送压缩内容到不支持它的客户端、忘记将压缩内容声明为已经进行了gzip
编码等)页面都会被破坏。错误并不会经常发生,但它们是必须考虑的边缘情形。
一种安全的方式是只为已经证实过支持压缩的浏览器提供压缩内容,如IE6+
和 Mozilla 5.0+
。这被称为浏览器白名单方式,使用这种方式,可能会失去为一小部分本来支持压缩的浏览器提压缩内容的机会。但其他选择————为不支持压缩的浏览器提供压缩内容,明显更为糟糕。
如果把代理缓存加进来后,处理这些边缘情形浏览器将变得更为复杂。你不可能和代理共享浏览器白名单配置。用于设置浏览器白名单的指令过于复杂,无法使用HTTP头进行编码。最佳做法是将User-Agent
作为代理的另一种评判标准添加到Vary
头中。
// 响应头
Vary: Accept-Encoding, User-Agent
不幸的是,User-Agent
有上千种不同的值,代理不太可能为其所代理的所有URL
缓存Accept-Encoding
和 User-Agent
的全部组合,另一种方式使用 Vary: *
或 Cache-Control: private
来禁用代理缓存,记住这是为所有浏览器禁用代理缓存,因此会增加你的带宽开销,因为代理无法缓存你的内容。
如果你拥有大量的、多变的用户群,能够应付较高的带宽开销,并且享有高质量的名声,请压缩内容并使用Cache-Control: private
来禁用代理缓存,但是避免了边缘情形缺陷。
如果你的网站用户很少,边缘情形的浏览器就不需要太多关注,或者你更注意带宽开销,请压缩内容并使用Vary: Accept-Encoding
,不仅可以通过压缩组件大小和利用代理缓存来改善用户体验,还能降低服务器端的带宽开销并提升代理处理的请求数量。
对web服务器配置进行简单的修改,压缩尽可能多的组件,就能显著的改善页面的反应速度。