# 干货!页面响应14条性能优化规则

# 性能黄金法则

页面响应时间作为性能指标,其中的10%-20%用于下载HTML文档(后端优化也只能优化这部分,且改动大),剩下的80%-90%用于前端资源下载解析,相较起后端优化,前端优化要更为明显。说明了在前端做性能优化的重要性。

# 性能优化规则

规则按优先级排序,是当下YSlow工具的分析依据

# 1、减少HTTP请求

# 图片地图(过时)

与CSS Sprites的区别,图片地图只能使用在img上,而CSS Sprites可作用div、span上,较为灵活;图片地图的图片必须是连续的,而CSS Sprites无此限制。

<img usemap="#nav" src="./pic_map.jpg" />
<map name="nav">
  <!-- coords值说明:sx,sy,ex,ey -->
  <area shape='rect' coords='0,0,31,31' href='home.html' title='Home' />
  <area shape='rect' coords='36,0,66,31' href='gift.html' title='Gift' />
  <area shape='rect' coords='71,0,101,31' href='cart.html' title='Cart' />
  <area shape='rect' coords='106,0,136,31' href='setting.html' title='Setting' />
</map>
1
2
3
4
5
6
7
8

# CSS Sprites

合并后的图片比分离的图片的总大小要小,因为降低了图片自身的开销(颜色表、格式信息等等)

<style>
  #navbar span{
    width: 31px;
    height: 31px;
    display:inline;
    float: left;
    background0-image:url(/pic_map.jpg);
  }
  .home{
    background-position: 0 0;
    margin-right: 4px;
    margin-left: 4px;
  }
  .gift{
    background-position: 32px 0;
    margin-right: 4px;
  }
  .cart{
    background-position: 64px 0;
    margin-right: 4px;
  }
  .setting{
    background-position: 96px 0;
    margin-right: 0;
  }
</style>
<div id="navbar">
  <a href="./home.html" title="Home" ><span class="home"></span></a>
  <a href="./gift.html" title="gift" ><span class="gift"></span></a>
  <a href="./cart.html" title="cart" ><span class="cart"></span></a>
  <a href="./setting.html" title="Setting" ><span class="setting"></span></a>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 内联图片

图片内容是一段base64数据字符串,格式如下,

data:[<mediaType>][;base64],data
1

一般是在外部样式表存放,以便在多页面实现缓存命中。图片数据不是很大可以使用该方式

# 脚本或样式文件的合并

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

# 2、使用CDN

# 由来

用户群增加时,一定会面临服务器放置地点不再适用的事实。若应用程序web服务器离用户更近,则一个http请求响应时间将缩短;若资源(如文档、样式、脚本、图片)web服务器离用户更近,则多个http请求响应时间将缩短。说明服务器存放位置与访问的用户地理位置的距离影响网络传输!!!

若采用分布式架构设计web应用程序,则需解决同步问题,如会话同步、数据库同步等。可采用另一种简单方式——CDN。CDN是一组分布在不同地理位置的web服务器,用于更加有效地向用户发布内容。

# 原理

通过测量网络可用度,选择网络阶跃数最小或响应时间最短的服务器向用户发布内容。

# 特点

1、缩短响应时间、备份、扩展存储、缓存、缓和web流量峰值压力

2、响应时间受其他网站流量的影响(因为CDN服务提供商在其所有客户之间共享web服务器组)

3、无法直接控制组件服务器,比如修改响应头需要服务提供商解决

4、CDN服务性能下降,工作质量也下降了(使用至少两个CDN服务提供商)

5、只支持发布静态内容,涉及动态需引入其他存储需求(数据库连接、状态管理、验证、硬件和OS优化),其复杂性超过CDN能力范畴。

# 使用方式

  • 用户使用代理配置浏览器;

  • 开发者使用不同域名修改组件服务器url

# 3、添加Expires请求头

# 原理

当浏览器检测到某个资源响应头带有Expires时,会将该资源缓存。只要资源未过期,浏览器会直接使用缓存,不发起任何请求。(不带expires头,浏览器也会做缓存,只是缓存策略走的是条件get请求,即协商缓存)

# 好处

条件GET请求虽然应用了缓存,但仍需要进行一次会话。而使用Expires响应头可以明确指出浏览器是否可以直接使用资源副本。

# 弊端

1、值是一个时间点,要求服务器和客户端时钟严格同步

2、过期日期需经常检查,且当时间超过有效期时服务器需重新提供新的时间点

为了解决expires存在问题,在HTTP1.1中出现了expires的替代品cache-control

取值可如下,表示资源可以被缓存多久。

cache-control:max-age=时长(s)
1

当时间(当前时间 - 开始请求时间,这一步浏览器帮你处理好了)是在缓存时间内,直接使用缓存。值是一个相对时间,避免了expires弊端。若二者同时出现,cache-control优于expires。

Apache服务器提供了mod_expires模块,以兼容expires和cache-control两种方式 传送门 (opens new window)

# 相关术语

空缓存与完整缓存:用户第一次访问网页时不会对HTTP请求数量产生任何影响,此时称空缓存;用户非第一次访问网页时对页面相关的资源都进行了缓存,此时称完整缓存。

revving:修订文件名,即给文件名附带版本号,避免使用缓存,获取最新。

# 4、服务器开启gzip压缩

# 相关的请求头、响应头

# 请求头,声明支持的压缩方式
Accept-Encoding:gzip,deflate
# 响应头,确刃使用的压缩方式
Content-Encoding:gzip
1
2
3
4

# 文件压缩

  • html、css、js、json适合压缩(个人是对超过10KB的文件进行处理)
  • 图片、pdf不适合压缩,影响CPU处理

# 代理缓存对gzip压缩的影响

  • 问题:请求第一次是由不支持gzip的浏览器发送给代理,代理缓存的是非gzip压缩的资源,当第二次是由支持gzip的浏览器发送,获取到的是非gzip压缩的资源;反过来,第一次请求是支持gzip的浏览器发送的,代理缓存了gzip的资源,当第二次请求是由不支持gzip的浏览器发送,此时拿到的是gzip的资源,页面会被“破坏”。也被称为边缘情形。

  • 解决方案

# 方案1:服务器使用响应头Vary
# 根据Accept-Encoding请求头的值返回不同的响应。当值为gzip时,返回压缩的资源,当Accept-Encoding没有指定时,返回未压缩的资源。代理为每个不同的值各自做了一份缓存。
Vary: Accept-Encoding
# (慎用)如果加上User-Agent,可以过滤不支持gzip低版本浏览器,但是会使代理缓存变复杂,结果可能导致禁用该缓存
Vary: Accept-Encoding,User-Agent


# 方案2:禁用代理缓存
# 方式一:适用于大量且多变的用户群,能应付较高的带宽开销,且享有高质量名声。默认推荐Vary:Accept-Encoding
Vary: *
# 方式二(推荐):
Cache-control: private
1
2
3
4
5
6
7
8
9
10
11
12

# 5、将样式表置于head

提问:在IE中,将样式表置于文档末尾,会比样式表置于head快?

解答:

先说结论,从用户视觉即页面内容首次呈现时间(所谓白屏时间)上,要慢。从响应时间指标上,要快。

再分析:将样式表置于文档末尾,会阻止浏览器对内容逐步呈现(即延迟显示所有可视化组件),也就是白屏时间长。

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

小结:

IE:样式表置于文档末尾->白屏时间长->浏览器阻止内容逐步呈现->防止出现FOUC

FireFox:样式表置于文档末尾->浏览器不阻止内容逐步呈现->白屏时间短->出现FOUC

无论是出现白屏,还是出现FOUC,都建议将样式表置于head。

题外话:由于历史原因,浏览器支持违反HTML规范的页面

<!-- 推荐引入样式方式 -->
<link rel="stylesheet" href="style.css" />
1
2

# 6、将脚本置于文档尾末

  • HTTP1.1规范建议浏览器从每个主机名并行下载两个资源(不同浏览器的限制数量可能不同)

  • 并行下载时使用多个不同域名,借此规避上面规范?经验得知,使用两个域名是可行的,但不建议更多,因为这会增加对每个域名的DNS解析耗时,而且也依赖于用户的带宽、CPU。

  • !!!下载脚本时不会并行下载其他资源,此时浏览器会禁用并行下载,即使使用了不同主机名。原因有二,一是脚本可能使用document.write修改页面内容,浏览器需要等待,确保页面正确布局,二是保证脚本能按正确顺序执行(保证顺序执行,不代表脚本也要顺序下载,浏览器已经做了改进,但是脚本的下载和执行仍旧会阻塞后面资源的下载)

# 7、避免CSS表达式(仅限于IE)

  • 示例
/* 仅IE支持 */
width: expression(document.body.clientWidth < 600? '600px': 'auto');
/* 其他浏览器 */
min-width: 600px;
1
2
3
4
  • 缺点

    CSS表达式更新频率高,在页面加载、事件监听,都会被触发。

  • 解决

// 一次性表达式
<style>p{background-color: expression(setColor(this))}</style>
<script>
function setColor(el){
  el.style.backgroundColor = new Date().getHours() % 2 ? '#f08a00' : '#bbd4ff';
}
</script>

// 事件处理器
function setMinWidth(){
  var pEl = document.getElementByTagName('p');
  for(var i = 0; i < pEl.length; i++){
    pEl[i].runtimeStyle.width = document.body.clientWidth < 600? '600px' : 'auto';
  }
}
if(-1 !== navigator.userAgent.indexOf('MSIE')){
  window.onresize = setMinWidth;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 8、使用外部JS和CSS

# 内联资源比外置快,那为什么还推荐使用外置方式?

  • 外置方式虽然可以充分利用并行下载,但是在HTTP请求数上存在差距
  • 即便如此,外置方式可以被浏览器缓存

# 如何提高缓存命中率?

  • 页面浏览量:用户浏览量UV越大,缓存命中率越高
  • 完整缓存率:为用户带来高完整缓存率,使用外部方式体验更好,若不大可能产生完整缓存,内联是更好的选择
  • 组件重用:做好边界处理。两种极端,是将所有页面合并为一个JS和CSS,还是为每个页面独立地提供一份JS和CSS。(在如今SPA应用盛行且网络发达下,通常是前者,当然独立的脚本可以独一份抽取)

# 内联与外部引入互补

  • 动态加载,即当页面加载完毕后动态创建script、link标签并插入

    动态导入、预加载preload(预测用户接下来可能访问的内容)这两种技术的实现也是类似

  • 通过cookie设置标识,是决定采用内联方式还是外部引入,虽然cookie状态与浏览器缓存不是对应关系(cookie有状态但已经内联,无状态但有外部引入即缓存)。不推荐

# 9、减少DNS查找

  • IP地址与域名的关系可以是多对一,能提高服务器的冗余度

  • 通常浏览器查找一个给定主机名的IP地址要花费20-120ms

  • DNS查找作缓存,提升性能。缓存可以是在ISP上,或用户本地计算机上。

    浏览器缓存是与操作系统缓存分离,当浏览器缓存被删除,才会通过操作系统(DNS客户端服务,提供了查看和刷新DNS服务命令ipconfig /displaydnsipconfig /flushdns)请求DNS查找,操作系统可能通过自身缓存来处理该请求,或者发送请求到远程服务上。

  • DNS记录包含了一个存活时间值(TTL),告知客户端可以对该记录缓存多久。

    一般浏览器会忽略该值,自己设置时间限制,而操作系统会考虑TTL。响应头存在Connection:keep-alive,重用连接,避免重复DNS查找,于是此时TTL、浏览器时间限制是不起效的。

    浏览器对缓存的DNS记录数量也有限制。

  • 一些知名网站,TTL值设置不是很大如1min、5min、10min,是为了提供快速故障转移

  • 客户端收到的DNS记录的平均TTL值只有最大TTL值的一半。因为DNS解析器自身也拥有与DNS记录相关的TTL,当浏览器进行DNS查找时,DNS解析器返回的时间是剩余时间

  • 影响DNS查找因素有DNS记录的存活时间TTL、浏览器对DNS记录缓存的数量限制、keep-alive持续时间

  • 减少主机名数量能避免DNS查找,降低响应时间,但也潜在减少页面并行下载数量,可能会增加响应时间。需要在主机名数量和并行下载数量做权衡,既保证减少DNS查找,又能充分利用并行下载特性

# 10、精简JS

  • 理解精简【JSMin】

    删除不必要的字符,如注释、无用的空白符(空格符、换行符、制表符),减小JS文件大小。

  • 混淆【Dojo Compressor】

    除了包括精简的处理,还改写代码,如变量名或函数名变得更简短,使用逗号表达式等等。

    但存在3个主要缺点:缺陷(过程中可能引入错误)、维护(一些不可改变的关键字需要做标记,防篡改)、调试难(阅读性差)

  • 精简CSS

    颜色值字符变小,如color:#333;

    0值不必带单位,如margin:0;

# 11、避免重定向

  • 响应状态码影响浏览器缓存,如何理解?一般来说,访问的资源地址当前曾访问过会走缓存,但是301、302重定向到响应头指定的Location,此时浏览器并不会对其做缓存,除非带了Expires或Cache-Control。

  • 常见的3xx响应码

    301 永久重定向

    302 临时重定向

    304 无修改,使用缓存

    305 使用代理

  • 前端实现重定向

<meta http-equiv='refresh' content='0;url=XXX'>
1
window.location.href = XXX;
1
  • 替代方案

    结尾处该补斜线则补,避免重定向

    远程调用替代重定向,“连接网站”;域名变更可通过CNAME(一条DNS记录,用于创建从一个域名指向另一个域名的别名)使两个主机名指向相同IP

    建立Referrer日志“跟踪内部流量”,避免重定向

    使用信标(一个HTTP请求,通过Image发送出去)“跟踪出战流量”,避免重定向

    “美化URL”是使用重定向另一原因,针对这点,暂无解决方法。

# 12、删除重复脚本

  • 损伤性能:重复的脚本请求(比如没做缓存会发起重复的脚本请求,做了缓存会进行重复的条件get请求)和重复的脚本执行多次

  • 如何避免插入重复脚本呢?需要一个脚本管理模块(大致原理是检测脚本是否登记插入,若有则停止操作,否则登记并插入,再检查脚本是否有依赖,若有则进行遍历,重复以上操作,直至不再有依赖,所有脚本被插入)

  • 现在的模块化已经可以避免这个问题,除非还采用传统的前端开发方式。

# 13、配置ETag

  • ETag全称是entity tag,中译为实体标识

  • 响应头会影响浏览器缓存,如何理解?比如条件GET请求

    条件GET请求(个人理解是协商缓存过程的一部分):若浏览器在缓存中保留了资源的一个副本,但不确定它是否有效,就会生成一个条件GET请求检查是否更新。若确认缓存的副本仍然有效,则继续使用缓存中的副本,响应更快体验更好。

  • 那么浏览器是如何知道缓存副本的有效性的?

    首先,浏览器通过上次资源访问时返回的响应头提供的Last-Modified,知道组件的最后修改时间。下次访问资源时带上If-Modified- Since传给服务器,若服务器返回的Last-Modified时间不变,则响应304,告知没有修改,直接使用缓存。以上是HTTP1.0,HTTP1.1则是改用ETag和If-None-Match

  • 优点

    解决高并发,需精确到秒级的问题。

    更为灵活,当资源依赖user-agent或accept-language时,此时就可以利用etag。

  • 缺点

    在服务器集群(多台相同服务器)下,且使用资源的某些属性(每台服务器都是不一样的)构造etag,当浏览器向另一台服务器发起条件GET请求时,etag是不匹配的,降低有效性验证的成功率(若有n台服务器,有效性验证率就是1/n,n越大,有效验证率越低)

  • 解决方案:基于不同服务器,修改配置,保证Etag在每台服务器上相同

    Apache1.3和2.x Etag格式为inode-size-timestamp,剔除inode部分

    IIS5.0和6.0 Etag格式为Filetimestamp:ChangeNumber,剔除ChangeNumber部分

# 14、使Ajax可缓存

  • 使用HTTPS,在确保用户数据的隐私性下,又使响应可缓存

# 小结

由于IE已经退出舞台,所以除去第7条规则,其余13条在当下仍然有效。

  • 减少HTTP请求
  • 使用CDN
  • 添加Expires请求头
  • 服务器开启gzip压缩
  • 将样式表置于head
  • 将脚本置于文档尾末
  • 使用外部JS和CSS
  • 减少DNS查找
  • 精简JS
  • 避免重定向
  • 删除重复脚本
  • 配置ETag
  • 使Ajax可缓存

以上内容是本人读《高性能网站建设指南》后做的总结。