Search This Blog

Monday 19 November 2012

gzip your Tomcat response

I was looking at the request and response headers in our application the other day. I noticed that the Browser request included the Accept-Encoding header. The values indicated my browser was capable of handling zipped responses.
However my response header indicated the returned content of the css files as text/css.
Response without using gzip - seen from Firebug
 As can be seen the request headers include an "Accept-Encoding" header which has a value of "gzip, deflate". This means my browser can handle compressed content. But my server is not serving it. Instead of a gzipped response, I have a plain text response sent from the server.
This was a bad thing as the larger response size meant more network time and therefore delayed response. I had to fix this. The solution is to simply train your server to send out a zipped response stream if the browser supports it.
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
        FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
    if(supportsGzip(httpServletRequest)) {
        CompressionResponseWrapper responseWrapper = new CompressionResponseWrapper(httpServletResponse);
        filterChain.doFilter(servletRequest, responseWrapper);
    } else {
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

private boolean supportsGzip(HttpServletRequest httpServletRequest) {
    boolean accepts=false;
    String encoding = httpServletRequest.getHeader("Accept-Encoding");
    if (null != encoding && encoding.indexOf("gzip")>-1) {
        accepts = true;
    }
    return accepts;
}
Here I added a filter to intercept requests. If the browser included the "Accepts-Encoding" header with value gzip, then I wrapped my response to use my own stream. Once request processing was complete, the filter would simply take the stream data from the wrapper, zip it and then write it to the actual Servlet Output Stream.
Issue solved. Or so I thought. Actually I had only fixed a very minor area of the problem. My servlet was configured to handle and return HTML content. But what about the static content in my page ? There were CSS includes, external Script files. These would be downloaded by the browser using direct GET calls. My filter wouldn’t work in this case.
What I needed was that the Server itself behave like a filter. If the requesting browesr supported compression, the server could compress and send the resource. Else it could simply write the requested resource as is.
I hunted for a solution online and found one exactly matching my requirements. This article even includes a good explanation of the concepts involved.
Accordingly I made the addition to my tomcat’s server.xml file:
<Connector port="8090" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8444"
    compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata"
     compressableMimeType="text/html,text/xml,text/javascript,text/css"/>
On restart the response was changed.
From 32.8 kb it came down to 5.8 kb.
However the same did not seem to work for my .js files.Consider the view for the jquery file:
In spite of including "text/javascript" in the list of compressableMimeTypes, my js file was not returned compressed.
I looked into the include element in my html file.
<script type="text/javascript" src="../js/jquery/jquery-1.7.2.js"></script>
The interesting thing was that the type I had specified in my script element was “text/javascript”. The compressableMimeTypes also included ”text/javascript”. However the content-type indicated in the Tomcat response was “application/javascript”.
Accordingly I modified my server.xml file:
<Connector port="8090" protocol="HTTP/1.1"
       connectionTimeout="20000"
       redirectPort="8444"
       compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata"
       compressableMimeType="text/html,text/xml,text/javascript,text/css,application/javascript"/>
A restart to Tomcat indicated that the js files had been compressed too.
So what was all this regarding “text/javascript” and “application/javascript” ? Was there any real difference between the two ? Why did Tomcat ignore my type attribute in the script element and add its own? I found some discussions on the same here.I produced a best answer below:
You should use application/javascript. This is documented in the RFC.
As far a browsers are concerned, there is no difference (at least in HTTP headers). 
This was just a change so that the text/* and application/* MIME type groups had a 
consistent meaning where possible. (text/* MIME types are intended for human 
readable content, JavaScript is not designed to directly convey meaning to humans).

Note that using application/javascript in the type attribute of a script element 
will cause the script to be ignored (as being in an unknown language) in some older 
browsers. Either continue to use text/javascript there or omit the attribute 
entirely (which is permitted in HTML 5).
This isn't a problem in HTTP headers as browsers universally (as far as I'm aware) 
either ignore the HTTP content-type of scripts entirely, or are modern enough to 
recognise application/javascript.
So this explains the javascript issue. A better option is to configure an apache instance to do the compression. However if you do no have an Apache instance sitting before Tomcat, then configuring Tomcat as shown above should bring the power of gzip to your web application.

4 comments:

  1. Good article, explained well.
    Thanks.

    ReplyDelete
  2. Hi,
    Where to add doFilter() and supportsGzip() method?

    ReplyDelete
  3. Hi
    Do you have any knowledge how to achieve it with angular ?
    Basically we are deploying angular in tomcat

    ReplyDelete