# 干货!页面响应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>
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>
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
一般是在外部样式表存放,以便在多页面实现缓存命中。图片数据不是很大可以使用该方式
# 脚本或样式文件的合并
多个脚本合并为一个脚本;多个样式表合并为一个样式表
# 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)
当时间(当前时间 - 开始请求时间,这一步浏览器帮你处理好了)是在缓存时间内,直接使用缓存。值是一个相对时间,避免了expires弊端。若二者同时出现,cache-control优于expires。
Apache服务器提供了mod_expires模块,以兼容expires和cache-control两种方式 传送门 (opens new window)
# 相关术语
空缓存与完整缓存:用户第一次访问网页时不会对HTTP请求数量产生任何影响,此时称空缓存;用户非第一次访问网页时对页面相关的资源都进行了缓存,此时称完整缓存。
revving:修订文件名,即给文件名附带版本号,避免使用缓存,获取最新。
# 4、服务器开启gzip压缩
# 相关的请求头、响应头
# 请求头,声明支持的压缩方式
Accept-Encoding:gzip,deflate
# 响应头,确刃使用的压缩方式
Content-Encoding:gzip
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
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" />
2
# 6、将脚本置于文档尾末
HTTP1.1规范建议浏览器从每个主机名并行下载两个资源(不同浏览器的限制数量可能不同)
并行下载时使用多个不同域名,借此规避上面规范?经验得知,使用两个域名是可行的,但不建议更多,因为这会增加对每个域名的DNS解析耗时,而且也依赖于用户的带宽、CPU。
!!!下载脚本时不会并行下载其他资源,此时浏览器会禁用并行下载,即使使用了不同主机名。原因有二,一是脚本可能使用
document.write
修改页面内容,浏览器需要等待,确保页面正确布局,二是保证脚本能按正确顺序执行(保证顺序执行,不代表脚本也要顺序下载,浏览器已经做了改进,但是脚本的下载和执行仍旧会阻塞后面资源的下载)
# 7、避免CSS表达式(仅限于IE)
示例
/* 仅IE支持 */
width: expression(document.body.clientWidth < 600? '600px': 'auto');
/* 其他浏览器 */
min-width: 600px;
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;
}
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 /displaydns
和ipconfig /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'>
window.location.href = XXX;
替代方案
结尾处该补斜线则补,避免重定向
远程调用替代重定向,“连接网站”;域名变更可通过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可缓存
以上内容是本人读《高性能网站建设指南》后做的总结。