DistChen

在 Web 程序中动态添加 GZIP 支持

大家都知道开启 GZIP 后的好处显而易见:提高网页的浏览速度。不管是精简代码,还是压缩图片都不如启用 GZIP 来的实在。GZIP 压缩效率非常高,通常可以达到70%的压缩率,也就是说,如果你的网页有30K,压缩之后就变成了9K左右,极大的减少了传输的数据量。web程序在进行性能优化的时候,都会启用GZIP支持。

下面一段话摘自《WEB性能优化之GZIP压缩》

GZIP压缩是一个经常被用到的WEB性能优化的技巧,它主要是对页面代码,CSS,Javascript,PHP等文件进行压缩,而且在压缩的前后,文件的大小会有明显的改变,从而达到网站访问加速的目的。

接下来我们就介绍一下什么是GZIP压缩,以及GZIP压缩是个什么概念。

GZIP网页压缩,是一种WEB服务器与浏览器之间共同遵守的协议,也就是说WEB服务器和浏览器都必须支持该技术才能实现GZIP压缩,而当下主流的浏览器都是支持GZIP压缩,包括IE6、IE7、IE8、IE9、FireFox、谷歌浏览器、Opera等,而常见的WEB服务器通常有Apache和IIS两种。

GZIP最早由Jean-loup Gailly和Mark Adler创建,用于UNIX系统的文件压缩。我们在Linux中经常会用到后缀为.gz的文件,它们就是GZIP格式的。目前,GZIP已经成为Internet上使用非常普遍的一种数据压缩格式,或者说一种文件格式。

下面介绍一下GZIP压缩时,WEB服务器与浏览器之间的协商过程如下:

  1. 首先浏览器请求某个URL地址,并在请求的开始部分头(head) 设置属性accept-encoding值为gzip、deflate,表明浏览器支持gzip和deflate这两种压缩方式(事实上deflate也是使用GZIP压缩协议,在之后的内容之我们会介绍二者之间的区别);
  2. WEB服务器接收到请求后判断浏览器是否支持GZIP压缩,如果支持就传送压缩后的响应内容,否则传送不经过压缩的内容;
  3. 浏览器获取响应内容后,判断内容是否被压缩,如果是压缩文件则解压缩,然后显示响应页面的内容。

HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。这一般是指 WWW服务器中安装的一个功能,当有人来访问这个服务器的网站时,服务器的这个功能就将网页内容压缩后传输到来访的电脑浏览器中显示出来。一般对纯文 内容可压缩到原大小的40%,这样以来文件的体积就减小了很多,传输速度也就快了。效果就是你点击网址后会很快的显示出来。当然这也会增加服务器的负载,一般的服务器中都会安装有这个功能模块。

GZIP压缩的比率往往在3倍到10倍,也就是本来90k大小的页面,采用压缩后实际传输的内容大小只有28至30K大小,这可以大大节省服务器的网络带宽,同时如果应用程序的响应足够快时,网站的速度瓶颈就转到了网络的传输速度上,因此内容压缩后就可以大大的提升页面的浏览速度。

在实际应用中,我们并不需要对网站所有文件都进行压缩,只需要对静态文件进行压缩就可以了,比如Javascript、CSS及和HTML文件。对其他文件进行压缩并不会给WEB性能带来太多的改观,并且对网站开启GZIP功能是需要牺牲部分服务器性能的。而且对于FLASH文件来说开启GZIP压缩之后还会影响其效果。

从服务器(tomcat、iis 、weblogic 等)级别可以启用 GZIP 支持,从代码级别也可以启用 GZIP 支持,下面介绍的是从代码级别来动态启用GZIP支持。

从代码级别动态启用GZIP支持

从代码级别动态启用 GZIP 支持有什么好处,哪些场景需要从代码级别启用 GZIP 支持?下面是我列出的几项:

  1. 对老系统优化。如果系统中引用的资源都是未压缩过的文件,那么当从服务器级别入手,还要修改程序中资源的引用方式为压缩过的文件。如果页面少,这些修改的工作量还可以接受;否则,就要改到哭;

  2. 静态资源多的网站。如果没有相应的工具批量处理,那么还要给每一个文件生成相应的压缩文件,这工作量也不小;

  3. 模块化加载的前端框架。现在很多的前端框架(dojo)或工具都以 AMD 的规范来编写,加载的时候根据模块的标识去动态引用对应的js文件。比如:当我 require(“a”)的时候,框架会去加载 a.js 文件,指向的还是未压缩过的文件。(当然,你可以修改框架的加载方式,但是这就意味着当框架升级的时候,你都要改一下加载方式)

从上面的几点来看,在代码级别动态启用GZIP支持还是有一定的意义的。我这里的处理方式就是在服务器端添加 Filter,哪些资源需要启用GZIP,就写相应的过滤规则,然后判断是否有对应的压缩文件,如果没有压缩文件,就先生成一个压缩文件,最后将压缩文件的数据返回去。因此,已有的前端程序不需要做任何的更改,服务器也不用任何的调整,压缩文件也不用预先生成,只是要在后台添加一个 Filter 并启用此 Filter。

注意:如果不做任何处理,返回压缩数据流,服务器给 response 添加的一些 header 策略就会失效,比如添加的 ETag、Last-mofified 等,因此需要自己处理缓存、响应的策略。可以参考我的另一篇文章《浏览器缓存与访问方式》

下面是效果截图:
未启用前
这里写图片描述
启用后
这里写图片描述

可以看到,当请求同一资源 Base.css,启用前后分别是 404 KB 和 46.7 KB,由此可以看到压缩率有多高,传输的数据减少的何止一丁点,所以我强烈推荐大家启用 GZIP 。

注意点

  1. response 要设置 Content-Encoding 和相应的 Content-Type;
  2. 生成压缩文件时候要考虑到并发;
  3. 要添加一些响应策略。否则你会发现,每次请求的时候,状态码都是200,内容也是重复传输,我这里没做任何处理才会这样。如果你不启用,你会发现第二次请求的时候,状态码是304,而且传输的大小是0,这是因为中间件添加了一些响应策略的,而这个 Filter 已经把这些策略给屏蔽掉了,所以需要自己补上这些策略。
    这里写图片描述

下面是我的一些实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>GZipFilter</filter-name>
<filter-class>distchen.gzip.DynamicGZIPFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>GZipFilter</filter-name>
<url-pattern>*.js</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>GZipFilter</filter-name>
<url-pattern>*.css</url-pattern>
</filter-mapping>
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
* Company: Dist
* Date:2016/8/25
* Author: DistChen
* Desc:
*/
package distchen.gzip;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.sun.org.apache.bcel.internal.generic.NEW;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.GZIPOutputStream;
public class DynamicGZIPFilter implements Filter {
private int BUFFER = 1024;
private String PREFIX = ".gz";
private ServletContext servletContext;
private Map<String, byte[]> gzipFileCache = new Hashtable();
private final Lock lock = new ReentrantLock();
public void init(FilterConfig filterConfig) throws ServletException {
this.servletContext = filterConfig.getServletContext();
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
String encoding=request.getHeader("Accept-Encoding");
if(encoding.indexOf("gzip")!=-1){
byte[] content = null;
String url = request.getRequestURI();
String filePath = url.substring(request.getContextPath().length());
if (filePath.endsWith(".js")) {
response.setContentType("text/javascript; charset=UTF-8");
} else if (filePath.endsWith(".css")) {
response.setContentType("text/css; charset=UTF-8");
}
if(this.gzipFileCache.containsKey(filePath)){
content = (byte[])this.gzipFileCache.get(filePath);
}else{
try {
lock.lock();
if(this.gzipFileCache.containsKey(filePath)){
content = (byte[])this.gzipFileCache.get(filePath);
}else{
content = getFileContent(filePath);
this.gzipFileCache.put(filePath, content);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
response.setHeader("Content-Encoding", "gzip");
ServletOutputStream servletOutputStream = response.getOutputStream();
servletOutputStream.write(content);
servletOutputStream.flush();
servletOutputStream.close();
}else{
filterChain.doFilter(request, response);
}
}
private byte[] getFileContent(String filePath) throws IOException{
File file = new File(this.servletContext.getRealPath("/")+File.separator+filePath+PREFIX);
if(!file.exists()){
generateGZIPFile(filePath);
}
InputStream fileInputStream = this.servletContext.getResourceAsStream(filePath+PREFIX);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(BUFFER);
int length = 0;
byte[] buf = new byte[BUFFER];
while (length >= 0) {
length = fileInputStream.read(buf);
if (length > 0) {
bytes.write(buf, 0, length);
}
}
fileInputStream.close();
return bytes.toByteArray();
}
private void generateGZIPFile(String filePath) throws IOException {
InputStream fis = null;
FileOutputStream fos = null;
GZIPOutputStream gos = null;
try {
fis = this.servletContext.getResourceAsStream(filePath);
fos = new FileOutputStream(this.servletContext.getRealPath("/")+File.separator+filePath+PREFIX);
gos = new GZIPOutputStream(fos);
int length;
byte[] data = new byte[BUFFER];
while((length = fis.read(data, 0, BUFFER))!=-1){
gos.write(data, 0, length);
}
gos.finish();
gos.flush();
gos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally{
fis.close();
fos.flush();
fos.close();
}
}
public void destroy() {
}
}

不足之处,请各位多多指教。

坚持原创技术分享,您的支持将鼓励我继续创作!