前端性能优化的14条法则

文章是高性能网站建设指南(Steve Souders)的总结。

性能黄金法则:

只有10%~20%的最终用户响应时间花在了下载HTML文档上。其余的80%~90%时间花在了下载页面中的所有组件上。

在这之后是14条提升性能的法则。这些规则按照常规的优先级顺序列出。特定的规则,适用性可能不同。所以不同的网站需要合理的选取规则。

规则1—-减少HTTP请求

图片地图

图片地图允许你在一张图片上关联多个URL。想必多张图片(多个URL)而言,一张图片只有一个HTTP请求。

图片地图实现方法有两种,一种是服务器端图片地图,一种是客户端图片地图

  • 服务器端图片地图是将所有点击提交到同一个URL,向其传递x、y坐标。web应用程序将坐标映射为适当的操作。

  • 客户端图片地图是通过map标签。下面是w3c的示例:

<img src="planets.jpg" border="0" usemap="#planetmap" alt="Planets" />

<map name="planetmap" id="planetmap">
  <area shape="circle" coords="180,139,14" href ="venus.html" alt="Venus" />
  <area shape="circle" coords="129,161,10" href ="mercur.html" alt="Mercury" />
  <area shape="rect" coords="0,0,110,260" href ="sun.html" alt="Sun" />
map>

客户端图片地图灵敏度太差,只适合矩形,而且坐标易出错。(我是没用过)

CSS Sprites(CSS雪碧图)

一句话概括就是将很多张小图片通过ps放到一张图片上。这样通过设置背景位置(background-position)在不同位置显示合适的图片。

同样,使用雪碧图也可以减少大量HTTP请求。

另一个令人惊奇的优点是,它还降低了下载量。很多人会认为合并后的图片比分离的图片的总和要大,因为合并后的图片中包含有附加的空白区域。实际上,合并后的图片会比分离后的图片的总和要小,这是因为它降低了图片自身的开销(颜色表、格式信息,等等)。(对图片要求不高,我使用iconfont)

内联图片

通过使用data:URL模式可以在web页面中包含图片但无需任何额外的HTTP请求。

data:[;base64],<data>

如:

background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7");

合并脚本和样式表

  • 多个脚本合并为一个脚本
  • 多个样式表合为一个样式表

对于模块化代码,合理选择组合是有必要的。

规则2—-使用内容分发网络(CDN)

如果应用程序Web服务器离用户更近,则一个HTTP请求的相应时间将会被缩短。
另一方面,如果组件web服务器离用户更近,则多个HTTP请求的相应时间将会被缩短。

优点

  • 内容分发网络(CDN)是一组分布在多个不同地理位置的web服务器,用于更加有效地向用户发布内容。通常只在讨论性能问题时会提到它的性能,但它还节省成本。

  • 除了缩短响应时间以外,CDN还可以带来其他优势。他们的服务包括备份、扩展存储能力和进行缓存。CDN还有助于缓和web流量峰值的压力,如在获取天气或股市新闻、浏览流行的体育或娱乐事件时。

缺点

  • 依赖CDN的一个缺点是你的响应时间可能会受到其他网站—-设置可能是你的竞争对手的影响。CDN服务提供商在其所有客户之间共享其web服务器组。

  • 无法直接控制组件服务器。比如修改HTTP响应头必须通过服务提供商来完成,而不是由你的工作团队完成。

  • 如果CDN性能下降,你的工作质量也会随之下降(不过你可以使用多个CDN服务提供商)。

规则3—-添加Expires头

今天的web页面都包含了大量的组件,并且数量在不断增长。页面的初访者会进行很多HTTP请求,但通过使用一个长久的Expires头,使这些组件可以被缓存。这会在后续的页面浏览中减少不必要的HTTP请求。长久的Expires头最常用与图片,但应该将其用在所有组件上,包括脚本、样式表的Flash。

Expires头

如下响应头(百度首页的某张图片):
Response Headers

Accept-Ranges:bytes
Age:792098
Cache-Control:max-age=2592000
Connection:keep-alive
Content-Length:0
Content-Type:image/png
Date:Mon, 06 Feb 2017 06:36:17 GMT
ETag:"5881c862-664"
Expires:Mon, 27 Feb 2017 02:34:38 GMT
Last-Modified:Fri, 20 Jan 2017 08:20:50 GMT
Ohc-Response-Time:1 0 0 0 0 0
Server:bfe/1.0.8.13-sslpool-patch

指定了Expires,即过期时间。但是同时又指定了Cache-Control,这时情况需要另算了。

max-age和mod_expires

HTTP 1.1引入了Cache-Control头来客服Expires头的限制。因为Expires头使用一个特定的时间,它要求服务器和客户端的时钟严格同步。另外,过期时间需要经常检查,并且一旦这一天到来了,还需要在服务器配置中提供一个新的日期。

换一种方式,Cache-Control使用max-age指定组件被缓存多久(单位是秒)。如果从组件被请求开始过去的秒数少于max-age,浏览器就是用缓存的版本,这就避免了额外的HTTP请求。如上述的max-age头将刷新窗设置为30天。

使用带有max-age的Cache-Control可以消除Expires的限制,但对于不支持HTTP 1.1的浏览器(还有吗?),如果仍然希望提供Expires头,可以同时提供两个响应头–Expires和Cache-Control。如果两者同时出现,HTTP规范规定max-age指令将重写Expires头。然而,如果你很尽职尽责,你仍然需要担心Expires带来的时钟同步和配置维护问题。

幸运的是,mod_expires Apache模块使你在使用Expires头时能够像max-age那样以相对的方式设置日期。

关于Cache-Control,不同的操作影响是不同的。见:

http://www.laruence.com/2010/03/05/1332.html

空缓存和完整缓存

“空缓存”和“完整缓存”指的是与页面相关的浏览器缓存的状态。如果你的页面中的组件没有放在缓存中,则缓存为“空”。浏览器的缓存可能包含来自其他网站的组件,但这对你的页面没有任何帮助。反之,如果你的页面中的可缓存组件都在缓存中,则缓存是“完整的”。

chrome中,如何找到缓存:

  • chrome://cache
  • chrome://version => 个人资料路径 => cache/media cache

不仅仅是图片

为图片使用长久的Expires头非常普遍,但这一最佳实践不应该仅限于图片。长久的Expires头应该包含任何不经常变化的组件,包括脚本、样式表和Flash组件。但是,HTML文件不应该使用长久的Expires头,因为它包含动态内容,这些内容需要经常被更新。

修订文件名

如果缓存的文件改变了,那么如何更新呢?当出现了Expires/Cache-Control头时,直到过期时间为止一直会使用缓存的版本。浏览器不会检查任何更新,直到过了过期日期。这也是为什么使用Expires/Cache-Control头能够显著减少相应时间—-浏览器直接从硬盘上读取组件而无需生成任何HTTP请求。

为了确保用户能够获取组件的最新版本,需要在所有HTML页面中修改组件的文件名。

最有效的解决方案是修改其所有链接,这样,全新的请求将从原始服务器下载最新的内容。

如果使用PHP、Perl等动态语言生成HTML页,一种简单的解决方案是为所有组件的文件名使用变量。使用这种方法,在页面中更新文件名时只需要简单地在某个地方修改变量(如改变版本号xxx_1.1.js)。

一个具有长久的Expires头的组件将会被缓存,在后续请求中浏览器直接从硬盘上读取它,避免HTTP请求。然而,如果一个组件没有长久的Expires头,它仍然会存储在浏览器的缓存中。在后续请求中,浏览器会检查缓存并发现组件已经过期(HTTP术语称为 陈旧)。为了提高效率,浏览器会向原始服务器发送一个条件GET请求。如果组件没有改变,原始服务器可以免于发送整个组件,而是发送一个很小的头,告诉浏览器可以使用其缓存的组件。

SSL

如果设置Cache-Control: no-store表明该相应因为数据隐私原因不会被缓存,这是相应不会被写到磁盘上。但是HTTP规范警告说不要依赖这一机制来确保数据的隐私性,恶意的或危险的缓存会完全忽略这个头。

处理数据隐私性更好的方法是使用安全通信协议如安全套接字层(Secure Layer, SSL)。SSL相应是可缓存的(在ff中只能用于当前浏览器回话),因此它提供了一种妥协—-在确保数据隐私性的同时在当前会话中缓存相应以改善用户体验。

规则4—-压缩组件

本规则通过减小HTTP响应的大小来减少相应时间。如果HTTP请求产生的响应包很小,传输时间就会减少,因为只需要将很小的包从服务器传递到客户端。

压缩是如何工作的

从HTTP 1.1开始,Web客户端可以通过HTTP请求中的Accept-Encoding头来标识对压缩的支持。
Request Headers

GET http://google.com/ HTTP/1.1
Host: google.com
Proxy-Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
X-Client-Data: CKy1yQEIirbJAQiltskBCMG2yQEIqZ3KAQ==
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Cookie: 省略

如果web服务器看到请求中有这个头,就会使用客户端列出的方法中的一种来压缩相应。web服务器通过相应中的Content-Encoding头来通知web客户端。

Content-Encoding:gzip

gzip是目前最流行和最有效的压缩方法。另一种压缩是deflate,但效果略逊且不太流行。

压缩什么

  • HTML文档、样式表、脚本(其实任何文本相应都可以)
  • 图片的PDF不应该被压缩,因为它们本来已经被压缩了。

配置

通过Apache配置文件、服务器端设置头部。

代理缓存

当浏览器直接与服务器进行通信时,一切都可以正常工作。
当浏览器通过代理来发送请求时,情况就复杂了。考虑一下情况:

  1. 对于某个url、一个请求来自于支持gzip压缩的浏览器。代理中没有缓存,会将请求转发到服务器。此时服务器响应一个压缩的文件,代理缓存这个文件,并发给服务器。
  2. 同样对于这个url,一个请求来自于**不支持**gzip压缩的浏览器,代理仍然将缓存的压缩文件返回给浏览器。这样就出问题了。

解决这一问题的方法是在web服务器的相应中添加Vary头。web服务器可以告诉代理根据一个或多个请求头来改变缓存的响应。由于压缩的决定是基于Accept-Encoding头的,因此需要在服务器的Vary响应头中包含Accept-Encoding。

这使得代理会缓存响应的多个版本,为Accept-Encoding请求头的每个值缓存一份。再根据不同的Accept-Encoding值返回给浏览器不同的文件。

如果配置浏览器白名单来决定响应是否压缩(通常在配置文件中),需要:

Vary: Accept-Encoding, User-Agent

不同User-Agent值非常多(目前几大主流浏览器还是可以数过来的),这样设置白名单不是很合理。

规则5—-将样式表放在头部

逐步呈现

关心前端性能的工程师们都希望页面能逐步地加载,也就是说,我们希望浏览器能够尽快显示内容。
将样式表放在文档底部会导致在浏览器中阻止更多内容呈现。为避免样式变化时重绘页面中的元素,浏览器会堵塞内容逐步呈现。在浏览器和用户等待位于底部的样式表时,浏览器会延迟显示任何可视化组件,我们称之为白屏

白屏

在某些浏览器中将CSS样式表放在底部会导致白屏。为了避免白屏,我们应该将样式表放在文档顶部的HEAD中(页面逐步呈现)。将样式表放在文档中有两种方式,使用link标签和@import规则。

<link rel="stylesheet" href="style1.css">
<style>
  @import url("style2.css")
style>

link的优点

  • 一个style块可以包含多个@import规则,但@import规则必须放在所有其他规则之前(否则不会加载)。

  • link除了语法简单外,还能带来性能上的收益。@import可能会导致白屏,即使放在HEAD中也是如此。
    使用@import规则会导致组件下载的无序性(可能最后下载导致白屏)。

无样式内容的闪烁

如果样式表仍在加载,构建呈现树就是一种浪费,因为所有样式表加载并解析完毕之前无需绘制任何东西。否则,在其准备好之前显示内容会遇到FOUC(无样式内容的闪烁, Flash of Unstyled Content)问题。

白屏是浏览器在尝试修改前端工程师所犯的错误—-将样式表放在文档比较靠后的位置。白屏是对FOUC问题的弥补。浏览器可以延迟呈现,直到所有的样式表都下载完之后,这就导致了白屏。反之,浏览器可以逐步呈现,但要承担闪烁的风险。这里没有完美的选择。所以作为前端工程师,所需要做的就是将样式表放在HEAD中。

规则6—-将脚本放在底部

脚本会带来两个问题:

  • 脚本会堵塞并行加载,无论放在哪里都是如此
  • 对于位于脚本以下的内容,逐步呈现都被堵塞了。将脚本放在页面越靠下的地方,意味着越多的内容能够逐步地呈现。

并行下载

浏览器会并行地执行HTTP请求。(F12->Network->Timeline - Start Time)
HTTP 1.1规范建议浏览器从每个主机名并行地下载两个组件。很多Web页面需要从一个主机名下载所有组件。

如果一个Web页面平均地将组件分别放在两个主机名下,整体响应时间可以减少大约一半(可以并行下载四个组件)。

每个主机并行下载两个组件只是一个建议。依据浏览器不同而不同。

http://www.iefans.net/qingqiu-bingfa-lianjieshu-xianzhi/

前端工程师预期依赖用户来修改浏览器设置,不如简单地使用DNS将组件放到多个主机名中。但使用并行下载不是没有开销的,这取决于你的带宽的CPU速度,过多的并行下载反而会降低性能。YaHoo!的研究表明,使用两个主机名比使用1、4/10个主机名能带来更好的性能(主机数目多会增加DNS解析耗时)。

脚本堵塞下载

并行下载的优点是很明显的。然而,在下载脚本时并行下载实际上是被禁用的(现代浏览器一般上是可以的,自己观察一下)。即使使用了不同的主机名,浏览器也不会启动其他的下载。

  • 其中一个原因是,脚本可能使用document.write来修改页面内容,因此浏览器会等待,以确保页面能够恰当地布局。
  • 另一个原因是为了保证脚本顺序可以按照正确的顺序执行。如果并行下载过个脚本,就无法保证相应是按照特定顺序到达浏览器的。

最差情况 将脚本放在顶部

会带来两个问题:

  • 堵塞页面内容的呈现
  • 堵塞其他组件的下载

正确放置

  • 脚本放在底部
  • 使用defer

规则7—-避免CSS表达式

CSS表达式是动态设置CSS属性的强大(但危险)方法。Internet Explorer从第5个版本开始支持CSS表达式.

  • 背景色轮换:
background-color: expression((new Date()).getHours() % 2 ? '#000' : '#fff');
  • 页面最小宽度
width: expression( document.body.clientwidth < 600 ? '600px' : 'auto'); // IE
min-width: 600px; // other

用其他方式代替CSS表达式。

规则8—-使用外部JavaScript和CSS

内联VS外置

纯粹而言,内联JavaScript和CSS会快一些,因为减少了HTTP请求。但是在实际中,还是使用外部文件会产生比较快的页面。这是因为JavaScript和CSS文件有机会被浏览器缓存起来。

页面查看

每个用户产生的页面查看越少,内联JavaScript和CSS的论据越强势。反之,如果用户能够产生很多的页面查看,浏览器很可能将(具有长久Expires/max-age)外部文件放在缓存中。使用外部文件带来的收益会随着每用户查看页面次数的增加而增加。

组件重用

如果网站中每个页面都使用了相同的JavaScript和CSS,使用外部文件可以提高这些组件的重用率。在这种情况下外部文件更具有优势,因为当用户在页面间导航时,JavaScript和CSS组件已经位于浏览器的缓存中了。相反,如果没有任何两个页面共享相同的JavaScript和CSS,重用率就会很低。

在实际情况中,需要在引用外部文件(可以缓存)和内联文件(减少HTTP请求)之间找到一个平衡点。一般来说,将页面划分成几种不同的页面类型,然后为每种类型创建单独的脚本和样式表。这比维护一个单独的文件要复杂,但通常比为每个页面维护不同的脚本和样式表要容易,并且对于给定的任意页面都只需下载很少多余的JavaScript和CSS。

加载后下载

对于作为多次页面查看中的第一次的主页,我们希望为主页内联JavaScript和CSS,但又能为所有后续页面查看提供外部文件。这可以通过在主页加载完成后动态下载外部组件来实现(通过load事件)。这能够将外部文件放到浏览器的缓存中以便用户接下来访问其他页面。

<script>
window.onload = function() {
  setTimeout(function(){
    var scriptEle = document.createElement('script');
    scriptEle.src = 'script1.js';
    document.body.appendChild(scriptEle);

    var linkEle = document.createElement('link');
    linkEle.rel = 'stylesheet';
    linkEle.type = 'text/css';
    linkEle.ref = 'css1.css';
    document.head.appendChild(linkEle);
  }, 1000)
}
script>

动态内联

// nodejs
http.createServer(function(req, res) {
    res.write('');
    res.end();
})

规则9—-减少DNS查找

Internet通过IP地址来查找服务器的。DNS将主机名映射到IP地址上。

然而,DNS也是开销。通常浏览器查找一个给定的主机名的IP地址要花费20~120毫秒。在DNS查找完成之前,浏览器不能从主机名那里下载任何东西。

DNS缓存和TTL

查看浏览器的DNS缓存(Chrome中在地址栏输入chrome://net-internals/#dns)。

在用户请求了一个主机名之后,DNS信息会留在操作系统的DNS缓存中(windows是“DNS Client”服务),之后对于该主机名的请求将无需进行过多的DNS查找,至少短时间内不需要。

浏览器也会拥有自己的缓存,和操作系统的缓存相分离。只要浏览器在其缓存中保留了DNS记录,它就不会麻烦操作系统来请求这个记录。只有当浏览器缓存丢弃了这个记录时,才会向操作系统询问地址。

服务器通过设置可以表明记录被缓存多久。查找返回的DNS记录包含了一个存活时间(Time-to-live,TTL)值。该值告诉客户端可以对该记录缓存多久。

尽管操作系统缓存会考虑TTL值,但浏览器通常会忽略该值,并设置它自己的时间限制。此外HTTP协议中的Keep-Alive特性可以同时覆盖TTL和浏览器的时间限制。换句话说,只要浏览器的Web服务器愉快地通信着,并保持着TCP连接打开状态,就没有理由进行DNS查找。

浏览器对缓存的DNS记录的数量也有限制,而不管缓存记录的时间。

windows上通过“DNS Client”服务管理DNS缓存。可以使用ipconfig命令来查看和刷新DNS Client服务。

ipconfig /displaydns
ipconfig /flushdns

重新启动也可以情况DNS Client缓存。重启浏览器会清空浏览器缓存,但不会清空DNS Client服务缓存。

在chrome中通过chrome://net-internals/#dns可查看dns记录缓存情况,有意思的是,chrome中的缓存似乎总是长期的,只有数量限制。
前端性能优化的14条法则_第1张图片

减少DNS查找

减少DNS查找意味着使用更少的主机名。使用更少的主机名又意味着减少了并行下载数量。如何权衡?
作者给出的建议是讲这些组件放在至少两个,但不要超过4个主机名下。这是在减少DNS查找和允许高度并行下载之间作出的很好的权衡。

规则10—-精简JavaScript

精简

精简是从代码中移除不必要的字符以减小其大小,进而改善加载时间的时间。在代码被精简后,所有的注释以及不必要的空白字符都将被移除。这能减小文件体积。

混淆

混淆是可以应用在源代码上的另外一种优化方式。和精简一样,它也会移除注释和空白,同时它还会改写代码。作为改写的一部分,函数和变量的名字将会被转换为更短的字符串,这时的代码更加精炼,也更难阅读。通常这样做是为了增加对代码反向工程的难度,但这对提高性能也有帮助,因为这笔精简更能减少代码的大小。

混淆的缺点

  • 由于混淆更加复杂,混淆过程本身很有可能引入错误。
  • 由于混淆会改变JavaScript符号,一次需要对任何不能改变的符号(例如API函数)进行标记,防止混淆器修改他们。
  • 经过混淆的代码很难阅读。这使得在产品环境中调试更加困难。

精简内联脚本

这也是可行的。

压缩和精简

gzip压缩可以减少文件体积70%左右。使用精简后再使用gzip压缩会带来更高的效率。

精简CSS

带来的节省小于精简JavaScript。

  • 移除注释和空白
  • 合并相同的类,移除不必要的类
  • 使用缩写(用#666代替#666666)
  • 移除不必要的字符串(0代替0px)

规则11—-避免重定向

重定向用于将用户从一个URL重新路由到另一个URL。重定向有很多种,301和302是最常用的两种。通常对HTML文档进行重定向,不过也可以用于组件(图片,脚本等)。重定向的原因有很多,包括网站重新设计、跟踪流量、记录广告点击和建立易于记忆的URL。但无论如何,重定向会使得页面变慢

当web服务器想浏览器返回一个重定向时,相应中就会拥有一个范围在3xx的状态码。这表示用户代理必须执行进一步操作才能完成请求。以下是几种3xx状态码—-
- 300 Multiple Choices(基于Content-Type)
- 301 Moved Permanently
- 302 Moved Temporarily(亦称Found)
- 303 See Other(对302的说明)
- 304 Not Modified
- 305 Use Proxy
- 306(不再使用)
- 307 Temporary Redirect(对302的说明)

“304 Not Modified”并不是真正的重定向—-它是用来相应条件GET请求,避免下载已经存在于浏览器缓存中的数据。

301和302是用的最多的。303和307是在HTTP 1.1规范中添加的,用来澄清对302的使用(滥用),但是几乎没人使用303和307。下面是301响应头的一个示例:

HTTP 1.1 301 Moved Permanently
Location: http://www.baidu.com
Content-Type: text/html

浏览器会自动将用户带到Location字段所给出的URL。重定向所需要的信息都出现在这个头中了。相应提通常是空的。301和302相应在实际中都不会被缓存,除非有附加的头—-如Expires或Cache-Control。

在meta refresh标签中可以在其content属性所指定的秒数后重定向用户:

<meta http-equiv="refresh" content="5; url=http://www.baidu.com">

JavaScript也可以进行重定向,将document.location设置为期望的URL即可。如果你必须要进行重定向,最好的技术是使用标准的3xx HTTP状态码。

重定向时如何损伤性能的

在重定向完毕并且HTML文档下载完毕之前,没有任何东西显示给用户。重定向延迟了HTML的加载。

重定向之外的其他选择

缺少结尾斜线

这是最长发生的重定向,也是开发人员最容易忽略的。因为它有充分的理由—-它允许自动索引(自动转移到默认的index.html)。并且能够获得与当前目录相关的URL(logo.gif)。然而,很多流行的web站点并不依赖自动索引,而是依赖特定的URL和处理器。另外,URL通常也与根目录相关而不是和当前目录相关。

注意当主机名后缺少结尾斜线时不会发生重定向。比如,http://www.baidu.com不会发生重定向。然而,通过检查请求,会发现URL是包含结尾斜线的。导致自动产生斜线的原因是,浏览器在进行GET请求时必须制定一些路径。如果没有路径,例如:http://www.baidu.com,它就会简单地使用文档根(/):

GET / HTTP 1.1

当缺少结尾斜线时发送重定向时很多web服务器的默认行为,包括Apache。

连接网站

想象一下web后端被重写的情形,这时新的实现中的URL很可能会有所不同。将用户从旧的URL转移到新的URL的最简单的方式就是重定向。
虽然重定向降低了开发的复杂性,但是这也损害了用户体验。整合两个后端还有其他的选择,但比起简单地重定向需要做更多的开发工作,不过这样非但不会损害用户体验,还能使之改善。

  • Alias、mod_rewrite、DirectorySlash(都是服务器端配置)要求除URL之外还要提交到一个接口(处理器或文件名),但易于实现。
  • 如果两个后端在同一个服务器上,则他们的代码很可能自己就能连接。例如,旧的处理器代码通过编程调用新的处理器代码。
  • 如果域名变了,可以使用一个CNAME(一条DNS记录,用于创建一个域名指向另一个域名的别名)让两个主机名指向相同的服务器。如果做到治理点,这里提到的技术(上面两点)就是可行的。

跟踪内部流量

重定向经常用于跟踪用户流量的流向。比如一个链接被重定向包装,单击这个链接时,返回一个301相应,Location指向真正的URL。这样就可以知道用户的流量去向。

另一种选择是通过referer日志来跟踪流量去向。每个HTTP请求都包含一个URL,表明从哪个页面发起的请求,也就是引用方(有的时候没有引用页,如用户键入URL或使用书签时)。我通过百度搜索打开一个链接,在这个链接网页请求头中发现:

Referer:https://www.baidu.com/link?url=IZv4coYEvm06hEpRFIDlwAB4faRFbtcX7Krs9YZ3HQCu1K0DdD3st5FuIsqXF-TuQSmngS27uZRwyUJUrt5RO_Gu5eDhB62FOl97vLZqjvK&wd=&eqid=dd07dd000002a56200000004589b210e

使用Refered日志避免了重定向,也就改善了相应时间。然而,这种方法的难处在于,要相对离开百度的人进行统计,百度就必须分析所有目标网站的日志进行分析。
对于内部流量—-也就是同一家公司的各个网站之间的流量—-值得通过建立Referer日志来避免重定向,以此节省响应时间。如果目标网站属于其他公司,就不可能分析Referer日志了。

跟踪出站流量

当你尝试跟踪用户流量时,你会发现链接可能将用户带离你的网站。在这种情况下,使用Referer就不太现实了。

跟踪出站流量除重定向之外的选择是使用信标(beacon)—-一个HTTP请求,其URL包含有跟踪信息。跟踪信息可以从信标web服务器的访问日志中提取出来。信标通常是一个1px x 1px的透明图片;不过204相应更优秀,因为它更小,从不会被缓存,而且绝对不会改变浏览器的状态。

美化URL

使用重定向可以是URL更加美观并且易于记忆。改变方式参见上述连接网站

规则12—-移除重复脚本

重复脚本

导致重复脚本有两个重要因素—-团队大小和脚本数量。不同的团队都要处理某个东西(比如cookie),于是他们都添加了脚本(cookie.js)。另外,脚本数量众多,也会造成混淆。

重复脚本损伤性能

重复脚本损伤性能的方式有两种—-不必要的HTTP请求和执行JavaScript所浪费的时间。

避免重复脚本

  • 在模板系统中实现一个脚本管理模块。huiv
  • 服务器端插入脚本时检查。

规则13—-配置ETag

ETag是什么?

实体标签(Entity Tag,ETag)时web服务器和浏览器用于确认缓存有效性的一种机制。在进入ETag的细节之前,我们先回顾一下组件是如何被缓存和确认的。

Expires头

浏览器下载组建时,会将它们保存到缓存中。下次使用这个组件中,如果这个组件时新鲜的,就从硬盘使用这个组件。是否是新鲜的取决于Expires/Cache-Control头。

条件GET请求

如果缓存的组件过期了(或者用户明确重新加载了页面),浏览器再重用它之前必须首先检查它是否仍然有效。这称作一个条件GET请求。不幸的是浏览器必须执行这个请求执行有效性检查,但这仍然比简单地下载所有已过期的组件的效率要高。如果浏览器缓存中的组件是有效的(即它能够和原始服务器上的组件相匹配),原始服务器不会返回整个组件,而是返回一个“304 Not Modified”状态码。

服务器在检测缓存的组件是否和原始服务器上的组件相匹配时有两种方式:

  • 比较最新修改日期
  • 比较实体标签

最新修改日期

浏览器通过If-Modified-Since请求头表明浏览器缓存的最新日期。
原始服务器通过Last-Modified响应头来返回组件的最新修改日期。

如果If-Modified-Since和Last-Modified的值一样,服务器会返回一个304相应,而不会再次传送需要的数据。
Request Headers

GET /5aV1bjqh_Q23odCf/static/superman/css/super_min_c53cfdce.css HTTP/1.1
Host: ss0.bdstatic.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/css,*/*;q=0.1
Referer: https://www.baidu.com/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8
If-None-Match: W/"5881c862-63c6"
If-Modified-Since: Fri, 20 Jan 2017 08:20:50 GMT

Response Headers

HTTP/1.1 304 Not Modified
Server: bfe/1.0.8.13-sslpool-patch
Date: Thu, 09 Feb 2017 01:14:24 GMT
Content-Type: text/css
Connection: keep-alive
ETag: W/"5881c862-63c6"
Last-Modified: Fri, 20 Jan 2017 08:20:50 GMT
Expires: Sun, 19 Feb 2017 09:04:01 GMT
Age: 1699234
Cache-Control: max-age=2592000
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Encoding: gzip
Ohc-Response-Time: 1 0 0 0 0 0

可以看出,If-Modified-Since和Last-Modified的值是匹配的。所以使用缓存文件。

实体标签

ETag提供了另外一种方式,用于检测缓存中的组件和原始服务器上的组件是否匹配(“实体”是“组件”的另一种称呼)。ETag在HTTP 1.1引入。ETag是唯一标识了一个组件的特定版本的字符串。唯一的格式约束是该字符串必须用引号引起来。原始服务器使用ETag响应头来指定组件的ETag。

ETag的加入为验证实体提供了比最新修改日期更为灵活的机制。例如,如果实体依赖User-Agent或Accept-Language头而改变,实体的状态可以反映在ETag中。

此后,如果浏览器必须验证一个组件,它会使用If-None-Match传回原始服务器。如果ETag是匹配的,就会返回一个304状态码。如上。

ETag带来的问题

ETag通常使用组件的某些属性来构造它,这些属性对于特定的、寄宿了网站的服务器来说是唯一的。当浏览器从一台服务器上获取了原始组件后又向另一台不同的服务器发起条件GET请求时,ETag是不会匹配的—-而对于使用服务器集群来说,这是很常见的一种情况。默认情况下,对于拥有多台服务器的网站来说Apache和IIS向ETag中嵌入的数据都会大大地降低有效性验证的成功率。

Apache1.3和2.x的ETag格式是inode-size-timestamp(上述不是)。文件系统使用inode来存储诸如文件类型、所有者、组和访问模式等信息。尽管在多台服务器上一个文件可能位于相同的目录、具有相同的文件大小、权限、时间戳等,从一台服务器到另一台服务器,其inode仍然是不同的。

如果ETag不匹配会让用户下载更多内容,用户体验会下降。同时,服务器开销也更大。如果有10台服务器,则用户只有10%的机会得到正确的304相应。其余90%的机会会得到200响应并下载所有数据。

ETag还降低了代理缓存的效率。代理后面的用户缓存的Tag经常和代理缓存的ETag不匹配。这导致不必要的请求被发送到原始服务器。用户和代理之间不会出现304相应,而是产生两个又慢又大的200相应—-一个是原始服务器到代理的,一个是从代理到用户的。ETag的默认格式还可能引入安全性弱点。

更重要的是,If-None-Match比If-Modified-Since具有更高的优先级。你可能希望如果ETag不匹配但最新修改日期是相同的,也能发送一个“304 Not Modified”相应,但实际并不是这样的。依据HTTP 1.1规范,如果请求中同时出现了这两个头,则原始服务器禁止(MUST NOT)返回304(Not Modified),除非请求头中的条件头字段全部一致。实际上如果没有If-None-Match反而会更好一些。

ETag—-用还是不用

如果你在多台服务器上寄宿你的网站,而且使用的是具有默认ETag配置的Apache或IIS,用户将面对缓慢的界面、服务器会具有很高的负载、会消耗大量的带宽、并且代理也不能有效地缓存内容。即使有长久的Expires/Cache-Control头,一旦用户“重新加载”或者“刷新”页面,还是会产生条件GET请求。ETag还是要被验证。

一种选择是对ETag进行配置,以利用其灵活的验证能力。如:


if(strops($_SERVER["HTTP_USER_AGENT"], "MSIE")) {
  header("ETag: MSIE");
} else {
  header("ETag: notMSIE");
}
?>

如果组件必须通过最新修改日期之外的一些东西来进行验证,则ETag是一种强大的方法。

如果无需自定义ETag,最好简单地将其移除。Apache和IIS都将ETag视为一个性能问题,并建议修改ETag的内容。可以移除inode值。然而,剩下的(大小,时间戳)是重复信息(Last-Modified和Content-Length头可以提供完全等价的详细),所以最好将ETag移除(还可以降低响应头大小)。

在Apache中,只需向Apache配置文件中简单地添加一行配置就可以移除ETag:

FileETag none

规则14—-使Ajax可缓存

异步与及时

Ajax是异步的,但并代表Ajax就是及时的。这取决于Ajax请求时被动的还是主动的。被动请求是为了将来使用而预先发起的。例如,在一个基于web的email客户端中,可能会使用被动请求在用户真正需要之前下载用户的地址簿。经过被动加载,当用户需要为一个Email消息添加地址时,客户端能确保地址簿已经存在于缓存中。主动请求是基于用户当前的操作而发起的。例如查找所有与用户搜索条件相匹配的Email消息。

对于主动请求的Ajax,即使是异步的,用户仍然可能需要等待相应。

现实中的Ajax缓存

其实Ajax缓存就是HTTP缓存。
以下HTTP响应头是可以用来做Ajax缓存的:

  • Expires: 应该被应用在你知道内容何时被修改的情况下。 例如,如果是股票价格您可能会设置一个在10秒后过期的数值。对于照片,你可以设置一个更长时间的Expires头,因为你指望它永远不改变。Expires头允许浏览器在一段时间内可以重复使用缓存内容,并避免任何不需要的同服务器的交互过程.

  • Last-Modified: 设置这个标记会通知浏览器可以使用If-Modified-Since头来产生一个条件GET请求以便检查其本地缓存。如果数据不需要更新,服务器将使用HTTP 304状态码来响应此请求

  • Cache-Control: 如果允许,这应该被设置为’public’,使其他用户可以在中间代理和缓存服务器上存储和共享数据,在Firefox上,这还将启用针对HTTPS的缓存

这里有一篇文章介绍Ajax缓存。

总结

关于这本书的进阶指南已经出来了。。有时间有机会再写吧。要找实习了。 2017.2.9

你可能感兴趣的:(前端大杂烩)