本文首次发布于 tinypoint`s blog, 作者 @张小点(tinypoint) ,转载请保留原文链接.
待完善
- 细化每一个优化方案,尽量每一个都给出相应的实例
内容部分
1.尽量减少HTTP请求数
http请求包括各种资源的下载,如图片,样式表,脚本,Flash等。减少资源数必然能够减少页面的HTTP请求数。方法如下
- 合并文件:包括将脚本文件合并成一个脚本,合并css文件等
- css sprites:将网站的背景图片整合到一张图片中,使用background-position进行定位(工具包括cssGaga, gulp, webpack等)具体办法可以参考我的这篇博客
- 图片映射:将多张图片合并成单张图片,总大小不变,但是减少了请求数,同时加速了页面加载。图片映射只有在图像在页面中连续的时候才有用,比如导航条。但是给image map设置坐标的过程既无聊又容易出错,用image map来做导航也并不容易,所以不推荐用这种方式。
- 图片转成base64格式(行内图片):
base64是一种网络上常用的8bit字节代码的编码方式,可以放在url中(比如<img>的src属性)。图片转换成base64代码可以减少http请求,因为图片以字符编码的形式直接被嵌入到了html中(加重了html,但是减少了http请求)。图片base64编码化后大小会变大,但是通过gzip优化以后基本差不多。所以适合较小的图片直接编码成base64,较大的图片则不建议。html5将图片转换成base64代码 中讲解了html5如何将图片转换成base64格式。
2.减少DNS查找
在浏览器地址栏输入网址后,浏览器需要通过DNS查询得到网站真实IP。而查找的是需要成本的。
减少DNS查找的方式就是进行DNS缓存。可以缓存在特定的缓存服务器(ISP/local area network维护),也可以是用户的计算机。DNS信息留存在操作系统DNS缓存中(在windows中就是 DNS Client Serve )。大多浏览器也会有有自己的缓存,独立于操作系统缓存。只要浏览器在自己的缓存里有某条DNS记录,它就不会向操作系统发DNS解析请求。
减少DNS请求数会减少并行下载数。避免DNS的查找虽然减少了响应时间,但并行下载数减少可能会增加响应时间。一个原则是组件可以分散在至少2个但不多于4个的不同域名下。这是对二者的妥协。
3.避免跳转
跳转用301(永久)或302(临时)状态码来达成。一个301响应http头的例子:
HTTP/1.1 301 Moved Permanently
Location: http://example.com/newuri
Content-Type: text/html
浏览器自动跳转到Location指定的路径。跳转所需的所有信息都在http头,所以http主体一般是空的。301 302响应一般不会被缓存,除非有额外的头部信息,比如Expires或Cache-Control指定要缓存。meta刷新标签或 JavaScript 也可以跳转,但如果真要跳转,3xx跳转更好,主要是保证返回键可用。
跳转显然拖慢了响应速度。因为在跳转的页面被获取前浏览器没什么能够渲染的,也没什么组件能下载。
最浪费的跳转之一发生在url尾部slash(/)缺失。比如http://astrology.yahoo.com/astrology会301跳转到http://astrology.yahoo.com/astrology/。这可以被Apache等服务器修复,用Alias,mod_rewrite等等。
4.让Ajax可缓存
动态创建的Ajax响应是可以被缓存的,通过设置Expires或者Cache-Control HTTP头可以将未过期或者未更改的资源缓存起来,以提高性能。另外,还有的其它方法是:
- gzip压缩
- 减少DNS查找
- 压缩JS
- 避免跳转
- 设置ETags
5.延迟加载组件
除了页面初始化必须的内容和组件,剩下的都可以延迟加载。JavaScript是理想的(延迟)候选者,可以分隔到onload事件之前和之后。比如拖放的js库可以延迟,因为拖动必须在页面初始化之后。其它可延迟的包括隐藏的内容,折叠起来的图片等等。
6.预加载组件
预加载与延迟加载的目的不同,预加载是为了利用浏览器的空闲时间来请求你将来会用到的组件。这样当用户访问下一个页面时,你会有更多的组件已经在缓存中,这会极大加快页面加载。
有几种预加载类型:
- 无条件预加载:一旦onload触发,你立即获取另外的组件。比如谷歌会在主页这样加载搜索结果页面用到的雪碧图。
- 有条件预加载:基于用户动作,你推测用户下一步会去哪里并加载相应组件。
- 预期的预加载:在发布重新设计(的网站)前提前加载。在旧网页预加载新网页的部分组件,那么切换到新网页时就不会是没有任何缓存了。
7.减少DOM数
一个复杂的页面意味着更多的内容要下载,以及更慢的dom访问。比如在有500dom数量的页面添加事件处理就和有5000dom数量的不同。
如果你的页面dom元素很多,那么意味着你可能需要删除无用的内容和标签来优化。
8.把组件分散到不同的域名
把组件分散到不同的域名允许你最大化并行下载数。由于DNS查询的副作用,最佳的不同域名数是2-4。
9.最小化iframe的数量。
iframe允许html文档被插入到父文档。
<iframe>优点
- 帮助解决缓慢的第三方内容的加载,如广告和徽章
- 安全沙盒
- 并行下载脚本
<iframe>缺点
- 成本高(资源和时间),即使是一个空的iframe
- 阻塞了页面的onload(最好是使用js动态添加)
- 非语义化(不利于SEO)
10.不要404
http请求是昂贵的,所以发出http请求但获得没用的响应(如404)是完全不必要的,并且会降低用户体验。
一些网站会有特别的404页面提高用户体验,但这仍然会浪费服务器资源。特别坏的是当链接指向外部js但却得到404结果。这样首先会降低(占用)并行下载数,其次浏览器可能会把404响应体当作js来解析,试图从里面找出可用的东西。
CSS部分
1.把样式放在顶部
样式表放在<head>里会让页面更快。这是因为把样式表移到<head>里允许页面逐步渲染。
关注性能的前端工程师希望页面被逐步渲染,这时因为,我们希望浏览器尽早渲染获取到的任何内容。这对大页面和网速慢的用户很重要。给用户视觉反馈,比如进度条的重要性已经被大量研究和记录。在我们的情况中,HTML页面就是进度条。当浏览器逐步加载页面头部,导航条,logo等等,这些都是给等待页面的用户的视觉反馈。这优化了整体用户体验。
把样式表放在文档底部的问题是它阻止了许多浏览器的逐步渲染,包括IE。这些浏览器阻止渲染来避免在样式更改时需要重绘页面元素。所以用户会卡在白屏。
HTML规范清楚表明样式应该在<head>里。
2.避免使用CSS表达式
用CSS表达式动态设置CSS属性,是一种强大又危险的方式。从IE5开始支持,但从IE8起就不推荐使用了。例如,可以用CSS表达式把背景颜色设置成按小时交替的:
background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );
CSS表达式的问题是它们可能比大多数人预期的计算的更频繁。它们不仅在页面载入和调整大小时重新计算,也在滚动页面甚至是用户在页面上移动鼠标时计算。比如在页面上移动鼠标可能轻易计算超过10000次。
要避免CSS表达式计算太多次,可以在它第一次计算后替换成确切值,或者用事件处理函数而不是CSS表达式。
3.选择舍弃@import
前面提到了一个最佳实践:为了实现逐步渲染,CSS应该放在顶部。
在IE中用@import与在底部用效果一样,所以最好不要用它。
4.避免使用IE的filter(过滤器)
IE专有的AlphaImageLoader过滤器用于修复IE7以下版本的半透明真彩色PNG的问题。这个过滤器的问题是它阻止了渲染,并在图片下载时冻结了浏览器。另外它还引起内存消耗,并且它被应用到每个元素而不是每个图片,所以问题(的严重性)翻倍了。
最佳做法是放弃AlphaImageLoader,改用PNG8来优雅降级
js部分
1.把脚本放到底部
脚本引起的问题是它们阻塞了并行下载。HTTP1.1规范建议浏览器每个域名下不要一次下载超过2个组件。如果你的图片分散在不同服务器,那么你能并行下载多个图片。但当脚本在下载,浏览器不会再下载其它组件,即使在不同域名下。
有些情况下把脚本移动到底部并不简单。比如,脚本中用了document.write来插入内容,它就不能被移动到底部。另外有可能有作用域问题。但大多数情况,有方法可以解决这些问题。
一个替代建议是使用异步脚本。defer属性表明脚本不包含document.write,是提示浏览器继续渲染的线索。不幸的是,Firefox不支持。如果脚本能异步,那么也就可以移动到底部。
2.把JavaScript和CSS放到外面
相比将JavaScript和CSS放到页面内,放在外部文件中可以让页面更快,因为JavaScript和CSS文件会被缓存在浏览器。HTML文档中的行内JavaScript和CSS在每次请求该HTML文档的时候都会重新下载。这样做减少了所需的HTTP请求数,但增加了HTML文档的大小。另一方面,如果JavaScript和CSS在外部文件中,并且已经被浏览器缓存起来了,那么我们就成功地把HTML文档变小了,而且还没有增加HTTP请求数。
3.压缩JS和CSS
压缩具体来说就是从代码中去除不必要的字符以减少大小,从而提升加载速度。代码最小化就是去掉所有注释和不必要的空白字符(空格,换行和tab)。在JavaScript中这样做能够提高响应性能,因为要下载的文件变小了。两个最常用的JavaScript代码压缩工具是JSMin和YUI Compressor,YUI compressor还可以压缩CSS。
混淆是一种可选的源码优化措施,要比压缩更复杂,所以混淆过程也更容易产生bug。在对美国前十的网站调查中,压缩可以缩小21%,而混淆能缩小25%。虽然混淆的缩小程度更高,但比压缩风险更大。
除了压缩外部脚本和样式,行内的<script>和<style>块也可以压缩。即使启用了gzip模块,先进行压缩也能够缩小5%或者更多的大小。JavaScript和CSS的用处越来越多,所以压缩代码会有不错的效果。
4.删除重复的脚本
在页面中引入相同的脚本两次会伤害性能。可能超出你的预料,美国top10网站的2家有重复脚本引入。两个主要因素造成同一页面引入相同脚本:团队大小和脚本数量。当确实引入重复脚本,会发出不必要的http请求和浪费js执行时间。
发出不必要的http请求发生在IE而不是Firefox。在IE,如果外部脚本引入两次且没有缓存,它会发出2个请求。即使脚本被缓存,刷新时也会发出额外请求。
除了增加http请求,时间被浪费在执行脚本多次上。不管IE还是Firefox都会执行多次。
一种避免多次引入脚本的方法是在模板系统实现一个脚本管理模块。
5.最小化DOM访问
用JS访问DOM元素是缓慢的,所以为了响应更好的页面,我们应该
- 缓存访问过的元素的引用
- 在DOM树外更新节点,然后添加到DOM树
- 避免用JS实现固定布局
6.用智能的事件处理器(事件委托)
有时候感觉页面反映不够灵敏,是因为有太多频繁执行的事件处理器被添加到了DOM树的不同元素上,这就是推荐使用事件委托的原因。如果一个div里面有10个按钮,应该只给div容器添加一个事件处理器,而不是给每个按钮都添加一个。事件能够冒泡,所以可以捕获事件并得知哪个按钮是事件源。
图片
1.优化图片
在设计师建好图片后,在上传图片到服务器前你仍可以做些事:
检查gif图片的调色板大小是否匹配图片颜色数。 可以把gif转成png看看有没有变小。除了动画,gif一般可以转成png8。 运行pngcrush或其它工具压缩png。 运行jpegtran或其它工具压缩jpeg。
2.优化CSS雪碧图
把图片横向合并而不是纵向,横向更小。 把颜色近似的图片合并到一张雪碧图,这样可以让颜色数更少,如果低于256就可以用png8. “Be mobile-friendly”并且合并时图片间的间距不要太大。这对图片大小影响不是太大,但客户端解压时需要的内存更少。100×100是10000个像素,1000×1000是1000000个像素。
3.不要在html中缩放图片
不要因为你可以设置图片的宽高就去用比你需要的大得多的图片。如果你需要
<img width="100" height="100" src="mycat.jpg" alt="My Cat" />
那么,就用100x100px的图片,而不是500x500px的。
4.用小的可缓存的favicon.ico
favicon.ico是放在服务器根目录的图片,它会带来一堆麻烦,因为即便你不管它,浏览器也会自动请求它,所以最好不要给一个404 Not Found响应。而且只要在同一个服务器上,每次请求它时都会发送cookie,此外这个图片还会干扰下载顺序,例如在IE中,当你在onload中请求额外组件时,将会先下载favicon。
所以为了缓解favicon.ico的缺点,应该确保:
足够小,最好在1K以下 设置合适的有效期HTTP头(以后如果想换的话就不能重命名了),把有效期设置为几个月后一般比较安全,可以通过检查当前favicon.ico的最后修改日期来确保变更能让浏览器知道。
cookie
1.给Cookie减肥
使用cookie的原因有很多,比如授权和个性化。HTTP头中cookie信息在web服务器和浏览器之间交换。重要的是保证cookie尽可能的小,以最小化对用户响应时间的影响。
- 清除不必要的cookie
- 保证cookie尽可能小, 以最小化对用户响应时间的影响
- 注意给cookie设置合适的域级别,以免影响其它子域
- 设置合适的有效期,更早的有效期或者none可以更快的删除cookie,提高用户响应时间
2.把组件放在不含cookie的域下
当浏览器发送对静态图像的请求时,cookie也会一起发送,而服务器根本不需要这些cookie。所以它们只会造成没有意义的网络通信量,应该确保对静态组件的请求不含cookie。可以创建一个子域,把所有的静态组件都部署在那儿。
如果域名是www.example.org,可以把静态组件部署到static.example.org。然而,如果已经在顶级域example.org或者www.example.org设置了cookie,那么所有对static.example.org的请求都会含有这些cookie。这时候可以再买一个新域名,把所有的静态组件部署上去,并保持这个新域名不含cookie。Yahoo!用的是yimg.com,YouTube是ytimg.com,Amazon是images-amazon.com等等。
把静态组件部署在不含cookie的域下还有一个好处是有些代理可能会拒绝缓存带cookie的组件。有一点需要注意:如果不知道应该用example.org还是www.example.org作为主页,可以考虑一下cookie的影响。省略www的话,就只能把cookie写到*.example.org,所以因为性能原因最好用www子域,并且把cookie写到这个子域下。
服务器
1.传输时用gzip等压缩组件
http请求或响应的传输时间可以被前端工程师显著减少。终端用户的带宽,ISP,接近对等交换点等等没法被开发团队控制,但是,压缩可以通过减少http响应的大小减少响应时间。
从HTTP/1.1开始,客户端通过http请求中的Accept-Encoding头部来提示支持的压缩:
Accept-Encoding: gzip, deflate
如果服务器看到这个头部,它可能会选用列表中的某个方法压缩响应。服务器通过Content-Encoding头部提示客户端:
Content-Encoding: gzip
gzip一般可减小响应的70%。尽可能去gzip更多(文本)类型的文件。html,脚本,样式,xml和json等等都应该被gzip,而图片,pdf等等不应该被gzip,因为它们本身已被压缩过,gzip它们只是浪费cpu,甚至增加文件大小。
2.避免图片src属性为空
src属性是空字符串的图片很常见,主要以两种形式出现:
<img src=””>
var img = new Image();
img.src = "";
这两种形式都会引起相同的问题:浏览器会向服务器发送另一个请求。
3.配置ETags
实体标签(ETags),是服务器和浏览器用来决定浏览器缓存中组件与源服务器中的组件是否匹配的一种机制(“实体”也就是组件:图片,脚本,样式表等等)。添加ETags可以提供一种实体验证机制,比最后修改日期更加灵活。一个ETag是一个字符串,作为一个组件某一具体版本的唯一标识符。唯一的格式约束是字符串必须用引号括起来,源服务器用相应头中的ETag来指定组件的ETag:
- HTTP/1.1 200 OK
- Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
- ETag: “10c24bc-4ab-457e1c1f”
- Content-Length: 12195
然后,如果浏览器必须验证一个组件,它用If-None-Match请求头来把ETag传回源服务器。如果ETags匹配成功,会返回一个304状态码,这样就减少了12195个字节的响应体。
GET /i/yahoo.gif HTTP/1.1
Host: us.yimg.com
If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT
If-None-Match: "10c24bc-4ab-457e1c1f"
HTTP/1.1 304 Not Modified
4.对Ajax用GET请求
Yahoo!邮箱团队发现使用XMLHttpRequest时,浏览器的POST请求是通过一个两步的过程来实现的:先发送HTTP头,在发送数据。所以最好用GET请求,它只需要发送一个TCP报文(除非cookie特别多)。IE的URL长度最大值是2K,所以如果要发送的数据超过2K就无法使用GET了。
POST请求的一个有趣的副作用是实际上没有发送任何数据,就像GET请求一样。正如HTTP说明文档中描述的,GET请求是用来检索信息的。所以它的语义只是用GET请求来请求数据,而不是用来发送需要存储到服务器的数据。
5.早一点刷新buffer(尽早给浏览器数据)
当用户请求一个页面,服务器一般要花200-500ms来拼凑整个页面。这段时间,浏览器是空闲的(等数据返回)。在php,有个方法flush()允许你传输部分准备好的html响应给浏览器。这样的话浏览器就可以开始下载组件,而同时后台可以继续生成页面剩下的部分。这种好处更多是在忙碌的后台或轻前端网站可以看到。
一个比较好的flush的位置是在head之后,因为浏览器可以加载其中的样式和脚本文件,而后台继续生成页面剩余部分。
<!-- css, js -->
</head>
<?php flush(); ?>
<body>
<!-- content -->
6.使用CDN(内容分发网络)
用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面。但具体要怎么做呢?
实现内容在地理位置上分散的第一步是:不要尝试去重新设计你的web应用程序来适应分布式结构。这取决于应用程序,改变结构可能包括一些让人望而生畏的任务,比如同步会话状态和跨服务器复制数据库事务(翻译可能不准确)。缩短用户和内容之间距离的提议可能被推迟,或者根本不可能通过,就是因为这个难题。
记住终端用户80%到90%的响应时间都花在下载页面组件上了:图片,样式,脚本,Flash等等,这是业绩黄金法则。最好先分散静态内容,而不是一开始就重新设计应用程序结构。这不仅能够大大减少响应时间,还更容易表现出CDN的功劳。
内容分发网络(CDN)是一组分散在不同地理位置的web服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。
7.添上Expires或者Cache-Control HTTP头
这条规则有两个方面:
- 对于静态组件:通过设置一个遥远的将来时间作为Expires来实现永不失效
- 多余动态组件:用合适的Cache-ControlHTTP头来让浏览器进行条件性的请求
网页设计越来越丰富,这意味着页面里有更多的脚本,图片和Flash。站点的新访客可能还是不得不提交几个HTTP请求,但通过使用有效期能让组件变得可缓存,这避免了在接下来的浏览过程中不必要的HTTP请求。有效期HTTP头通常被用在图片上,但它们应该用在所有组件上,包括脚本、样式和Flash组件。
浏览器(和代理)用缓存来减少HTTP请求的数目和大小,让页面能够更快加载。web服务器通过有效期HTTP响应头来告诉客户端,页面的各个组件应该被缓存多久。用一个遥远的将来时间做有效期,告诉浏览器这个响应在2010年4月15日前不会改变。
Expires: Thu, 15 Apr 2010 20:00:00 GMT`
如果你用的是Apache服务器,用ExpiresDefault指令来设置相对于当前日期的有效期。下面的例子设置了从请求时间起10年的有效期
ExpiresDefault "access plus 10 years"
移动端
1.保证所有组件都小于25K
这个限制是因为iPhone不能缓存大于25K的组件,注意这里指的是未压缩的大小。这就是为什么缩减内容本身也很重要,因为单纯的gzip可能不够。
2.把组件打包到一个复合文档里
把各个组件打包成一个像有附件的电子邮件一样的复合文档里,可以用一个HTTP请求获取多个组件(记住一点:HTTP请求是代价高昂的)。用这种方式的时候,要先检查用户代理是否支持(iPhone就不支持)。
结语
雅虎总结了35条优化方案,接下来的就是在项目中去实际使用它们,我会在后期为每一条给出一个简洁的实例,帮助大家理解和使用,今天就先到这了,886。
参考