压缩构建的数据资源
数据压缩是提高 Web 站点性能的一种重要手段。对于有些文件来说,高达 70% 的压缩比率可以大大减低对于带宽的需求。随着时间的推移,压缩算法的效率也越来越高,同时也有新的压缩算法被发明出来,应用在客户端与服务器端。
HTTP 响应数据压缩
压缩 JS、CSS
这里所说的压缩指的是去除换行空格之类的压缩,文件内容不变。
使用 Gzip 压缩文本
浏览器和服务器之间会使用主动协商机制。浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中, Vary 首部中至少要包含 Accept-Encoding ;这样的话,缓存服务器就可以对资源的不同展现形式进行缓存。
下面是一个请求响应的 HTTP 报文示例:
GET /encrypted-area HTTP/1.1
Host: www.example.com
Accept-Encoding: gzip, deflate
HTTP/1.1 200 OK
Date: Tue, 27 Feb 2018 06:03:16 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Accept-Ranges: bytes
Content-Length: 438
Connection: close
Content-Type: text/html; charset=UTF-8
Content-Encoding: gzip
压缩图片
在图片优化章节中详细说明。
HTTP 请求数据压缩
头部数据压缩
HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。
HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
请求体数据压缩
前面我们介绍了 HTTP 协议中的 Accept-Encoding/Content-Encoding 机制。这套机制可以很好地用于文本类响应正文的压缩,可以大幅减少网络传输,从而一直被广泛使用。但 HTTP 请求的发起方(例如浏览器),无法事先知晓要访问的服务端是否支持解压,所以现阶段的浏览器没有压缩请求正文。
有一些通讯协议基于 HTTP 做了扩展,他们的客户端和服务端是专用的,可以放心大胆地压缩请求正文。例如 WebDAV 客户端就是这样。
实际的 Web 项目中,会存在请求正文非常大的场景,例如发表长篇博客,上报用于调试的网络数据等等。这些数据如果能在本地压缩后再提交,就可以节省网络流量、减少传输时间。本文介绍如何对 HTTP 请求正文进行压缩,包含如何在服务端解压、如何在客户端压缩两个部分。
开始之前,先来介绍本文涉及的三种数据压缩格式:
- DEFLATE,是一种使用 Lempel-Ziv 压缩算法(LZ77)和哈夫曼编码的压缩格式。详见 RFC 1951;
- ZLIB,是一种使用 DEFLATE 的压缩格式,对应 HTTP 中的 Content-Encoding: deflate。详见 RFC 1950;
- GZIP,也是一种使用 DEFLATE 的压缩格式,对应 HTTP 中的 Content-Encoding: gzip。详见 RFC 1952;
Content-Encoding 中的 deflate,实际上是 ZLIB。为了清晰,本文将 DEFLATE 称之为 RAW DEFLATE,ZLIB 和 GZIP 都是 RAW DEFLATE 的不同 Wrapper。
下面是一个简单示例。
- 压缩请求正文数据
var rawBody = 'content=test';
var rawLen = rawBody.length;
var bufBody = new Uint8Array(rawLen);
for(var i = 0; i < rawLen; i++) {
bufBody[i] = rawBody.charCodeAt(i);
}
var format = 'gzip'; // gzip | deflate | deflate-raw
var buf;
switch(format) {
case 'gzip':
buf = window.pako.gzip(bufBody);
break;
case 'deflate':
buf = window.pako.deflate(bufBody);
break;
case 'deflate-raw':
buf = window.pako.deflateRaw(bufBody);
break;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '/node/');
xhr.setRequestHeader('Content-Encoding', format);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.send(buf);
在 Node 中解压请求正文中的数据
var http = require('http');
var zlib = require('zlib');
http.createServer(function (req, res) {
var zlibStream;
var encoding = req.headers['content-encoding'];
switch(encoding) {
case 'gzip':
zlibStream = zlib.createGunzip();
break;
case 'deflate':
zlibStream = zlib.createInflate();
break;
case 'deflate-raw':
zlibStream = zlib.createInflateRaw();
break;
}
res.writeHead(200, {'Content-Type': 'text/plain'});
req.pipe(zlibStream).pipe(res); }).listen(8361, '127.0.0.1');
实际使用还需要匹配具体的服务器,比如 nginx、Apache 等。