社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
最近在做前端的一些事情。
使用echart绘图。遇到一个问题,就是用ajax 接收后端返回的json数据。测试发现速度很慢,调试发现后端返回的数据有54.7M,ajax接收时间在32-43秒左右,如图:
项目使用spring mvc框架,服务端使用@ResponseBody 自动打包 HttpServletResponse的返回内容,return HashMap,返回类型是application/json
这是在使用Ehcache 后的结果。令笔者想不到的是,返回的数据竟有54.7M大小,由于前端等待时间较长,因此需要做些优化。
首先,有哪些优化手段呢?ajax的格式是这样的:
$.ajax({
type: "POST",
url: '**/getModelData',
data: {jobId:jobId},
dataType:'json',
cache: false,
async: true,
success: function(data){
//
}
});
可以从同步/异步,cache入手。然而,异步通常用于加载dom,并不适用这里,网上一些异步方式讨论的也跟这里无关。笔者把cache 设置为true后,速度并没有提高。按理说,cache在接收第一次同样的数据后,会把数据临时缓存,下一次请求速度会快一些,实际发现,请求仍然是在返回后端的数据。没有看出明显提升。这让笔者有点奇怪。
既然无效,可不可以用一个js全局变量,临时存储后端返回的数据呢?这里每一个请求返回的数据大小都在几十M的规模,多请求几次,页面临时内存会有达到几百M的可能,这样是不是有些笨拙?
总之,并没有使用这样方式。剩下还有几种方式:
(1)压缩
(2)缓存
(3)服务端做优化。
首先是压缩。这是比较好的思路。tomcat,spring mvc,nginx 都提供压缩配置,主流的压缩格式是Gzip,恰好以上三者都提供。这里并没有用到nginx,所以,只考虑spring mvc和tomcat。
spring mvc 使用Gzip 需要一个GZIPResponseWrapper 类来继承HttpServletRespose,另外,fliter层需要GZIPFilter 实现Filter接口,简单说,就是再封装HttpServletResponse.
具体代码如下:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class GZIPFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (req instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String ae = request.getHeader("accept-encoding");
if (ae != null && ae.indexOf("gzip") != -1) {
GZIPResponseWrapper wrappedResponse = new GZIPResponseWrapper(response);
chain.doFilter(req, wrappedResponse);
wrappedResponse.finishResponse();
return;
}
chain.doFilter(req, res);
}
}
public void init(FilterConfig filterConfig) {
// noop
}
public void destroy() {
// noop
}
}
public class GZIPResponseWrapper extends HttpServletResponseWrapper {
protected HttpServletResponse origResponse = null;
protected ServletOutputStream stream = null;
protected PrintWriter writer = null;
public GZIPResponseWrapper(HttpServletResponse response) {
super(response);
origResponse = response;
}
public ServletOutputStream createOutputStream() throws IOException {
return (new GZIPResponseStream(origResponse));
}
public void finishResponse() {
try {
if (writer != null) {
writer.close();
} else {
if (stream != null) {
stream.close();
}
}
} catch (IOException e) {}
}
public void flushBuffer() throws IOException {
stream.flush();
}
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called!");
}
if (stream == null)
stream = createOutputStream();
return (stream);
}
public PrintWriter getWriter() throws IOException {
if (writer != null) {
return (writer);
}
if (stream != null) {
throw new IllegalStateException("getOutputStream() has already been called!");
}
stream = createOutputStream();
writer = new PrintWriter(new OutputStreamWriter(stream, "UTF-8"));
return (writer);
}
public void setContentLength(int length) {}
}
public class GZIPResponseStream extends ServletOutputStream {
protected ByteArrayOutputStream baos = null;
protected GZIPOutputStream gzipstream = null;
protected boolean closed = false;
protected HttpServletResponse response = null;
protected ServletOutputStream output = null;
public GZIPResponseStream(HttpServletResponse response) throws IOException {
super();
closed = false;
this.response = response;
this.output = response.getOutputStream();
baos = new ByteArrayOutputStream();
gzipstream = new GZIPOutputStream(baos);
}
public void close() throws IOException {
if (closed) {
throw new IOException("This output stream has already been closed");
}
gzipstream.finish();
byte[] bytes = baos.toByteArray();
response.addHeader("Content-Length",
Integer.toString(bytes.length));
response.addHeader("Content-Encoding", "gzip");
output.write(bytes);
output.flush();
output.close();
closed = true;
}
public void flush() throws IOException {
if (closed) {
throw new IOException("Cannot flush a closed output stream");
}
gzipstream.flush();
}
public void write(int b) throws IOException {
if (closed) {
throw new IOException("Cannot write to a closed output stream");
}
gzipstream.write((byte)b);
}
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
if (closed) {
throw new IOException("Cannot write to a closed output stream");
}
gzipstream.write(b, off, len);
}
public boolean closed() {
return (this.closed);
}
public void reset() {
//noop
}
}
这是别人写好的,也是可用的。不过相对这个问题,改动比较大,改完还需要调试。因此,并没有采用。
有没有改动小一点的? tomcat,nginx也内置了Gzip压缩配置方式:
<Connector port="8888" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="21000"
redirectPort="28080"
maxThreads="500"
minSpareThreads="50" maxIdleTime="60000
URIEncoding="UTF-8"
compression="on"
compressionMinSize="50"
noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/csv,application/javascript" />
加上去之后,发现没有效果。原因是compressableMimeType=”text/html,text/xml,text/plain,text/javascript,text/csv,application/javascript”
并不支持这里的数据类型,这里的数据类型是:application/json
在浏览器调试器可查看数据类型:
觉得有点奇怪,这里用的是tomcat7,是否不支持application/json类型?又或者这个问题本身很少见,ajax就是用来传输少量数据的?于是又在stackoverflow上面找相关讨论。
找到一个帖子,发现一个逗比,他的服务端返回的数据高达2GB,问题跟笔者的类似。
链接:
https://stackoverflow.com/questions/47991007/compress-and-send-large-string-as-spring-http-response
同行讨论说有提供Gzip压缩的,有提供其它方式优化的,但是一下子没看明白,改动比较大。之后又看到这样一段话:
说的正是tomcat压缩方式,不巧的是,笔者没有注意到,他说的是tomcat默认支持这些数据类型,隐含意思是,不止这些数据类型。
而笔者误解tomcat只支持这些数据类型,不支持application/json 的Gzip压缩。
带着这个误解,笔者又查了一些资料,也没有找到简单的方式。既然没找到,笔者想是不是加上去试一下看看,于是便加上去,发现果然有效果,如图:
而且压缩率惊人,压缩了17倍。有点怀疑,又用echart绘了图,并没发现数据异常。数据传输速度,提高了2-10秒。应该是解压的步骤也会耗时。
回去后,找了下笔者之前做的资料,发现Gzip压缩之前就有做过,按理说,也做过类似的优化方面的思考。可是,时间一长,反而什么也不记得了。所以还是写下来吧。
(2)缓存
可是即便这样,浏览器要对gzip数据解压,耗时也是挺大的。还需要再优化。想了几个小时,笔者尝试使用js 全局变量数组,把服务端的数据暂时存在全局变量,这样第一次请求跟之前一样,之后请求会快很多。问题是,这个会增加页面内存消耗,虽然有限制存取的数据对象个数。还是没有解决第一次请求的速度。这是全局变量(包含一个modelData)的占用内存:
然后笔者又查询html5的函数,发现localStorage 可以尝试。localStorage 容量比cookie,session 都要大,有5M。尝试了一下,由于笔者这里的数据解压后会有几十M,所以localStorage 不适合。
不过笔者发现一个别人写好的js 库,是对localStorage 的一个应用。
链接地址:
https://github.com/WQTeam/jquery-ajax-cache
(3)服务器端优化
剩下的便只有服务器端优化。思考再三,笔者尝试减少服务器端返回的数据大小。经过分析,发现,是有减小空间的。于是针对每次请求,尽量只返回必要的数据,那些该次请求没有用到的数据就不返回,经过整理,返回的json数据有明显减小,反映在前端,就是响应变快很多。这是优化后的:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!