记一次ajax接收后端大量数据的优化 - Go语言中文社区

记一次ajax接收后端大量数据的优化


最近在做前端的一些事情。

使用echart绘图。遇到一个问题,就是用ajax 接收后端返回的json数据。测试发现速度很慢,调试发现后端返回的数据有54.7M,ajax接收时间在32-43秒左右,如图:
后端返回的数据54.7m

项目使用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
  }
}

参考链接:
http://www.javablog.fr/javaweb-gzip-compression-protocol-http-filter-gzipresponsewrapper-gzipresponsewrapper.html

这是别人写好的,也是可用的。不过相对这个问题,改动比较大,改完还需要调试。因此,并没有采用。

有没有改动小一点的? 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压缩的,有提供其它方式优化的,但是一下子没看明白,改动比较大。之后又看到这样一段话:

这里写图片描述

链接:
https://stackoverflow.com/questions/21410317/using-gzip-compression-with-spring-boot-mvc-javaconfig-with-restful?noredirect=1&lq=1

说的正是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数据有明显减小,反映在前端,就是响应变快很多。这是优化后的:
这里写图片描述

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/cafebar123/article/details/80037589
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-04-18 19:35:02
  • 阅读 ( 838 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢