网页加载优化

Author Avatar
Hongxu 5月 04, 2018

网页加载速度优化

背景

最近做项目的时候,原本打开比较快的网页由于 CUP 的性能问题导致一样的代码在另一块芯片上打开速度明显变慢。

这套代码是台湾的同事做的,由于某些原因做了一半转接到我们手上。原本 Broadcom 芯片的机器网页打开速度比较快,换到只有他 1/4 处理速度的 Realtek 芯片的机器上首屏加载白屏时间变得比较长,这才开始优化网页代码结构。

但是网页优化技巧是相同的。所以这里也把了解的一些网页优化的技巧做一个整理。

雅虎网页优化守则

内容部分

1. 尽量减少HTTP请求次数

HTTP 请求的原理就不在这里展开细说,总之每一次的 HTTP 如果没有 Connection: keep-alive 的话,就是重新建立一次 TCP。这个开销是可以降低的。(使用 Connection: keep-alive 建立一次 TCP 连接之后一直使用该连接保持会话。直到连接关闭。)

网页的加载速度,大部分都是在下载或是等待下载页面上的不同组件:图片,样式,脚本等。减少这些组件的数量就能够减少页面 HTTP 数量。

a. 合并文件

将css,js文件等合并

b. 使用CSS Sprite

CSS Sprite 减少图片请求数量,多用于许多小而繁多的 icon 图片。将多个图片合并为同一个使用的时候用 background-imagebackground-position 来控制图片的显示。

c. Base64 编码图片

行内图片Base64编码用 img: src: url() 的方式把图片嵌入到页面中,这样会增加 css 的体积但是减少了 HTTP 请求的数量。使用 Base64 的返回只限一些体积较小的图片。如果体积较大转为 Base64编码 后的体积远远大于原图片体积。就会适得其反。所以一般来说转码后超过 1k(取决于你团队对于css/js 体积和对减少 HTTP 请求带来的效益的权衡)就不太适合 Base64编码

2. 减少 DNS 查找

浏览器 DNS 的查找顺序是 浏览器缓存 → 系统缓存 → 路由器缓存 → ISP(互联网服务提供商)DNS缓存 → 根服务器 → 顶级域名服务器 → 主域名服务器 → 保存结果

具体解析过程可以后续再开一文。

减少不同主机名的数量同时也减少了页面能够并行下载的组件数量,避免DNS查找削减了响应时间,而减少并行下载数量却增加了响应时间。我的原则是把组件分散在2到4个主机名下,这是同时减少DNS查找和允许高并发下载的折中方案。

3. 避免重定向

重定向用 301 和 302 状态码。
下面是 301 状态码的 HTTP 头:

HTTP/1.1 301 Moved Permanently

Location: http://example.com/
Content-Type: text/html

浏览器会自动跳转到 Location 域指明的 URL。重定向需要的所有信息都在 HTTP 头部,而响应体一般是空的。其实额外的 HTTP 头比如 Expires 和 Cache-Control 也表示重定向。除此之外还有别的跳转方式,如:refresh 元标签和 javascript, 但如果必须要做重定向,最好用标准的 3xx HTTP 状态码。

重定向会降低用户体验,在用户和 HTML 文档之间插入重定向会延迟页面上的所有东西,页面无法渲染,组件也无法下载,直到浏览器得到 HTML 文档。

4. 让 Ajax 可以缓存

确保Ajax请求具有长久的Expires头

5. 延迟加载组件

将首页渲染所必须的组件加载出来,不必须的组件在首页渲染完成后再加载。

6. 预加载组件

通过预加载组件,利用浏览器空闲时间来请求将来用户会用到的组件,当用户开始访问后续文档时,这些组件已经被缓存起来,所以在用户看来页面会加载得更快。

7. 减少 DOM 元素的数量

一个复杂的页面意味着要下载更多的字节,而且用JavaScript访问DOM也会更慢。

8. 跨域分离组件

分离组件可以最大化并行下载,但要确保只用不超过2-4个域,因为存在DNS查找的代价。例如,可以把HTML和动态内容部署在 www.example.org ,而把静态组件分离到 static1.example.org。

9. 尽量少用 iframe

用iframe可以把一个HTML文档插入到父文档里,重要的是明白iframe是如何工作的并高效地使用它。

iframe 的优点:

  • 引入缓慢的第三方内容,比如标志和广告
  • 安全沙箱
  • 并行下载脚本

iframe的缺点:

  • 代价高昂,即使是空白的iframe
  • 阻塞页面加载
  • 非语义

杜绝 404

HTTP请求代价高昂,完全没有必要用一个HTTP请求去获取一个无用的响应(比如404 Not Found),只会拖慢用户体验而没有任何好处。

CSS 部分

我觉得这一 part 完全是为了填 IE 的坑。哈哈哈。

11. 避免使用 CSS 表达式(IE)

用CSS表达式动态设置CSS属性,是一种强大又危险的方式。从IE5开始支持,但从IE8起就不推荐使用了。例如,可以用CSS表达式把背景颜色设置成按小时交替的:

background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );

(但是 CSS 新标准似乎打算为 CSS 提供比 calc 更强大的能力,期待(*❦ω❦))

为了实现逐步渲染,CSS应该放在顶部。在IE中用@import与在底部用 <link> 效果一样,所以最好不要用它。

13. 避免使用滤镜(IE)

IE专有的AlphaImageLoader滤镜可以用来修复IE7之前的版本中半透明PNG图片的问题。在图片加载过程中,这个滤镜会阻塞渲染,卡住浏览器,还会增加内存消耗而且是被应用到每个元素的,而不是每个图片,所以会存在一大堆问题。

最好的方法是干脆不要用AlphaImageLoader,而优雅地降级到用在IE中支持性很好的PNG8图片来代替。如果非要用AlphaImageLoader,应该用下划线hack:_filter来避免影响IE7及更高版本的用户。

14. 把样式放在顶部

把样式表放到文档的HEAD部分能让页面看起来加载地更快。这是因为把样式表放在head里能让页面逐步渲染。

关注性能的前端工程师想让页面逐步渲染。也就是说,我们想让浏览器尽快显示已有内容,这在页面上有一大堆内容或者用户网速很慢时显得尤为重要。

js 部分

15. 去除重复脚本

页面含有重复的脚本文件会影响性能,IE会产生不必要的HTTP请求,在IE中,如果一个不可缓存的外部脚本被页面引入了两次,它会在页面加载时产生两个HTTP请求。即使脚本是可缓存的,在用户重新加载页面时也会产生额外的HTTP请求。

16. 尽量减少 DOM 的访问

用JavaScript访问DOM元素是很慢的,所以,为了让页面反应更迅速,应该:

  • 缓存已访问过的元素的索引
  • 先“离线”更新节点,再把它们添到DOM树上 (documentFragment)
  • 避免用JavaScript修复布局问题

17. 用智能的时间处理器

事件委托

18. 把脚本放在底部

脚本会阻塞并行下载,HTTP/1.1官方文档建议浏览器每个主机名下并行下载的组件数不要超过两个,如果图片来自多个主机名,并行下载的数量就可以超过两个。如果脚本正在下载,浏览器就不开始任何其它下载任务,即使是在不同主机名下的。

有时候,并不容易把脚本移动到底部。举个例子,如果脚本是用document.write插入到页面内容中的,就没办法再往下移了。还可能存在作用域问题,在多数情况下,这些问题都是可以解决的。

一个常见的建议是用推迟(deferred)脚本,有DEFER属性的脚本意味着不能含有document.write,并且提示浏览器告诉他们可以继续渲染。不幸的是,Firefox不支持DEFER属性。在IE中,脚本可能被推迟,但不尽如人意。如果脚本可以推迟,我们就可以把它放到页面底部,页面就可以更快地载入。

19. 把 js 和 CSS 放到外面

用外部文件可以让页面更快,因为JavaScript和CSS文件会被缓存在浏览器。HTML文档中的行内JavaScript和CSS在每次请求该HTML文档的时候都会重新下载。这样做减少了所需的HTTP请求数,但增加了HTML文档的大小。另一方面,如果JavaScript和CSS在外部文件中,并且已经被浏览器缓存起来了,那么我们就成功地把HTML文档变小了,而且还没有增加HTTP请求数。

20. 压缩 js 和 CSS

去除代码中不必要的字符减少文件体积,提升加载速度。 Yahoo 在对美国前十的网站调查中,压缩可以缩小21%,而混淆能缩小25%。虽然混淆的缩小程度更高,但比压缩风险更大。

除了压缩外部脚本和样式,行内的 <script><style> 块也可以压缩。即使启用了gzip模块,先进行压缩也能够缩小5%或者更多的大小。JavaScript和CSS的用处越来越多,所以压缩代码会有不错的效果。

图片部分

21. 优化图片

尝试把GIF格式转换成PNG格式,看看是否节省空间。在所有的PNG图片上运行pngcrush(或者其它PNG优化工具)

当然在 2018 年的今天,已经有了更多可以优化的策略,webp? svg? fonticon? … 等等

22. 优化 CSS Sprite

  • 在Sprite图片中横向排列一般都比纵向排列的最终文件小
  • 组合Sprite图片中的相似颜色可以保持低色数,最理想的是256色以下PNG8格式
  • “对移动端友好”,不要在Sprite图片中留下太大的空隙。虽然不会在很大程度上影响图片文件的大小,但这样做可以节省用户代理把图片解压成像素映射时消耗的内存。100×100的图片是1万个像素,而1000×1000的图片就是100万个像素了。

23. 不要用HTML缩放图片

不要因为在HTML中可以设置宽高而使用本不需要的大图。如果需要

<img width="100" height="100" src="cat.jpg" alt="Cat" />

那么图片本身(cat.jpg)应该是100x100px的,而不是去缩小500 x 500px的图片。

24. 用小的可缓存的 favicon.ico

favicon.ico是放在服务器根目录的图片,它会带来一堆麻烦,因为即便你不管它,浏览器也会自动请求它,所以最好不要给一个404 Not Found响应。而且只要在同一个服务器上,每次请求它时都会发送cookie,此外这个图片还会干扰下载顺序,例如在IE中,当你在onload中请求额外组件时,将会先下载favicon。

所以为了缓解favicon.ico的缺点,应该确保:

  • 足够小,最好在1K以下
  • 设置合适的有效期HTTP头(以后如果想换的话就不能重命名了),把有效期设置为比较安全的长度,可以通过检查当前favicon.ico的最后修改日期来确保变更能让浏览器知道。

cookie部分

使用cookie的原因有很多,比如授权和个性化。HTTP头中cookie信息在web服务器和浏览器之间交换。重要的是保证cookie尽可能的小,以最小化对用户响应时间的影响。

  • 清除不必要的cookie
  • 保证cookie尽可能小,以最小化对用户响应时间的影响
  • 注意给cookie设置合适的域级别,以免影响其它子域
  • 设置合适的有效期,更早的有效期或者none可以更快的删除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写到这个子域下。

移动端部分

27. 保证所有组件都小于25K

这个限制是因为iPhone不能缓存大于25K的组件,注意这里指的是未压缩的大小。这就是为什么缩减内容本身也很重要,因为单纯的gzip可能不够。

28. 把组件打包到一个复合文档里

把各个组件打包成一个像有附件的电子邮件一样的复合文档里,可以用一个HTTP请求获取多个组件(记住一点:HTTP请求是代价高昂的)。用这种方式的时候,要先检查用户代理是否支持(iPhone就不支持)。

服务器部分

29. Gzip组件

gzip 可以大幅减少文本类的数据大小 html, css, js 等体积会大幅减少。

从HTTP/1.1开始,web客户端就有了支持压缩的Accept-Encoding HTTP请求头。

Accept-Encoding: gzip, deflate

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

Content-Encoding: gzip

30. 避免图片src属性为空

Image with empty string src属性是空字符串的图片很常见,主要以两种形式出现:

<img src="">
var img = new Image();
img.src = "";

这两种形式都会引起相同的问题:浏览器会向服务器发送另一个请求。

31. 配置 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

对 Ajax 使用 GET 请求

Yahoo!邮箱团队发现使用XMLHttpRequest时,浏览器的POST请求是通过一个两步的过程来实现的:先发送HTTP头,在发送数据。所以最好用GET请求,它只需要发送一个TCP报文(除非cookie特别多)。IE的URL长度最大值是2K,所以如果要发送的数据超过2K就无法使用GET了。

POST请求的一个有趣的副作用是实际上没有发送任何数据,就像GET请求一样。正如HTTP说明文档中描述的,GET请求是用来检索信息的。所以它的语义只是用GET请求来请求数据,而不是用来发送需要存储到服务器的数据。

33. 尽早清空缓冲区

当用户请求一个页面时,服务器需要用大约200到500毫秒来组装HTML页面,在这期间,浏览器闲等着数据到达。PHP中有一个flush()函数,允许给浏览器发送一部分已经准备完毕的HTML响应,以便浏览器可以在后台准备剩余部分的同时开始获取组件,响应耗时主要在后台方面时,这么做会更有优势

较理想的清空缓冲区的位置是HEAD后面,因为HTML的HEAD部分通常更容易生成,并且允许引入任何CSS和JavaScript文件,这样就可以让浏览器在后台还在处理的时候就开始并行获取组件。

34. 使用CDN(内容分发网络)

用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面。但具体要怎么做呢?

实现内容在地理位置上分散的第一步是:不要尝试去重新设计你的web应用程序来适应分布式结构。这取决于应用程序,改变结构可能包括一些让人望而生畏的任务,比如同步会话状态和跨服务器复制数据库事务(翻译可能不准确)。缩短用户和内容之间距离的提议可能被推迟,或者根本不可能通过,就是因为这个难题。

记住终端用户80%到90%的响应时间都花在下载页面组件上了:图片,样式,脚本,Flash等等,这是业届黄金法则。最好先分散静态内容,而不是一开始就重新设计应用程序结构。这不仅能够大大减少响应时间,还更容易表现出CDN的功劳。

内容分发网络(CDN)是一组分散在不同地理位置的web服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。

35. 添上Expires或者Cache-Control HTTP头

  • 对于静态组件:通过设置一个遥远的将来时间作为Expires来实现永不失效
  • 多余动态组件:用合适的Cache-ControlHTTP头来让浏览器进行条件性的请求

浏览器(和代理)用缓存来减少HTTP请求的数目和大小,让页面能够更快加载。web服务器通过有效期HTTP响应头来告诉客户端,页面的各个组件应该被缓存多久。用一个遥远的将来时间做有效期,告诉浏览器这个响应在2010年4月15日前不会改变。