Introduction
Yahoo has a great performance analysis tool in flavor of a Firefox addon: YSlow (yes, you need to install the -also great- Firebug addon first). The YSlow site has already explained all of the best practices in detail here.
Yahoo's explanations are in general clear enough for the average Java web application developer, but when the YSlow's Server category comes into the picture, Yahoo unfortunately only gives examples based on Apache HTTP server and PHP and in a few cases also IIS. In this article I'll "translate" the relevant subcategories into the Java Servlet approach based on Apache Tomcat 6.0. As a bonus, a few more best practices are added and explained in detail.
Back to top
Use a Content Delivery Network
This is the first rule of the YSlow's Server category. Well, the idea is nice, but this is in my opinion not a "must". Having a secondary domain (no, not a subdomain) for pure static content is a more general practice to gain performance in serving static content. A webbrowser is namely restricted to have a certain maximum amount of simultaneous open connections on a single domain. In the older browser versions this is usually limited to 2 and ranges nowadays around 10-15 connections. This can also be changed using a simple regedit (MSIE) or by editing about:config (Firefox). Those kind of tweaks are usually only done by the more advanced users with an above average knowledge of the software they use.
So, to give a broader area of visitors a better performance experience, it may be better to have a secondary domain for pure static content only. E.g. onedomain.com for JSP files and anotherdomain.com for CSS/JS/Flash/etc files. Or of course such a CDN as suggested by Yahoo, but again, a CDN for private static data is in my opinion a bit nonsensicial. After all, if you respect the performance rules for static content the correct way, then the static content will actually only be requested whenever really needed, so this makes a secondary domain or CDN more superfluous. Or you must have a webapplication which needs to serve a lot of non-layout-related images, such as photography.
For 3rd party public static content it's however definitely worth the effort to link it to a CDN which is provided by themselves, if any. For example jQuery offers several CDN hosts. It's a win-win situation for both your server and the client.
Back to top
Add an Expires or a Cache-Control Header
This is the second rule of the YSlow's Server category. A very good point. The Expires header prevents the browser to re-request the same static content (JS/CSS/images/etc) everytime, which is only a waste of the available time, connections and bandwidth. When you're serving static content from public webcontent in Tomcat, then the DefaultServlet is responsible for serving the content. It unfortunately does nothing with the Expires header. Although it supports the Last-Modified headers, this costs effectively a HEAD request which is already one connection and request too much when the content is actually not changed after all. You can however override the DefaultServlet with an own implementation as outlined here. How to do it effectively is already covered by the earlier FileServlet article at this blog. This servlet is a well suited solution for the second, third as well as the fourth rule of the YSlow's Server category.
About the cache-control header for dynamic content, the general practice is that we just want to avoid caching of dynamic content, especially the pages containing forms or the pages in restricted area. You can do that by adding the following response headers to the base controller Servlet or Filter of your webapplication:
... response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1. response.setHeader("Pragma", "no-cache"); // HTTP 1.0. response.setDateHeader("Expires", 0); // Proxies. ...
There is a little story behind the no-store and must-revalidate attributes of the cache-control header: some webbrowsers (including Firefox) doesn't cache the page when those attributes are omitted! According to the HTTP specification only the no-cache should have been sufficient. But OK, now we at least have the 'magic' three headers which should work for all decent webbrowsers and proxies.
Back to top
Use Query String with a timestamp to force re-request
The Expires and Cache-Control headers are useful, but .. with a (too) far-future expiration date or maximum age, the client won't check for any updates on the static resource anymore until the expire date has passed, or you clear the browser cache, or you do a hard-refresh (CTRL+F5)! A common practice is then to append an unique query string to the URL of the static content denoting a timestamp of the last file modification or the server startup time, so that the browser is forced to re-request it whenever the query string changes.
Determining the last modification time on every request is more expensive than just determining the server startup time only once in application's lifetime. It is generally sufficient to do so. Whenever the server restarts, the browser will send a HEAD request to check if there are any updates. Assuming that your server doesn't restart every minute or so, this doesn't harm that much. Here's an example of how to do it using a ServletContextListener:
package mypackage; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; /** * Configure the webapplication context. This is to be placed in the application scope. * As far now this example only sets the startup time. * @author BalusC * @see http://balusc.blogspot.com/2009/09/webapplication-performance-tips-and.html */ public class Config implements ServletContextListener { // Constants ---------------------------------------------------------------------------------- private static final String CONFIG_ATTRIBUTE_NAME = "config"; // Properties --------------------------------------------------------------------------------- private long startupTime; // Actions ------------------------------------------------------------------------------------ /** * Obtain startup time and put Config itself in the application scope. * @see ServletContextListener#contextInitialized(ServletContextEvent) */ public void contextInitialized(ServletContextEvent event) { this.startupTime = System.currentTimeMillis() / 1000; event.getServletContext().setAttribute(CONFIG_ATTRIBUTE_NAME, this); } /** * @see ServletContextListener#contextDestroyed(ServletContextEvent) */ public void contextDestroyed(ServletContextEvent event) { // Nothing to do here. } // Getters ------------------------------------------------------------------------------------ /** * Returns the startup time associated with this configuration. * @return The startup time associated with this configuration. */ public long getStartupTime() { return this.startupTime; } }
Just add it as a listener to the web.xml the usual way:
... <listener> <listener-class>mypackage.Config</listener-class> </listener> ...
Here is an example of how to use it in JSP:
... <link rel="stylesheet" type="text/css" href="/static/style.css?${config.startupTime}"> <script type="text/javascript" src="/static/script.js?${config.startupTime}"></script> ...
As a side-note, if you're using the aforementioned FileServlet as well, then you can in theory postpone the default expire time more. For example 1 year (365 days):
... private static final long DEFAULT_EXPIRE_TIME = 31536000000L; // ..ms = 365 days. ...
Back to top
Add LastModified timestamp to CSS background images
Appending query string with a timestamp to static CSS files is nice, but .. this doesn't cover the CSS background images! Those counts each as a separate request. If you don't append a timestamp query string to them, then they won't be checked for any updates. How to handle it may differ per environment, so I'll only describe my general approach to give the idea. You might need to finetune it further to suit your environment. I myself use a batch job using YUI Compressor (yes, it's a Java API!) to minify all CSS and JS files before deploy. After getting the minified result, regexp is used to find all background images in the CSS source and File#lastModified() is used to get the last modification timestamp from it and finally the originals will be replaced. Here's a basic example of the Minifier -keep in mind, this may needed to be modified to suit your environment:
package mypackage; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.yahoo.platform.yui.compressor.CssCompressor; /** * The Minifier. * @author BalusC * @see http://balusc.blogspot.com/2009/09/webapplication-performance-tips-and.html */ public class Minifier { // Actions ------------------------------------------------------------------------------------ /** * Minify all CSS files on given basePath + cssPath to the given basePath + minPath and append * lastmodified timestamps to CSS background images relative to the given basePath. * @param basePath The base path of static content. * @param cssPath The path of all CSS files, relative to the given basePath. * @param minPath The path of all minified CSS files, relative to the given basePath. * @throws IOException If something fails at I/O level. */ public static void minifyCss(String basePath, String cssPath, String minPath) throws IOException { for (File cssFile : new File(basePath + cssPath).listFiles()) { if (cssFile.isFile()) { File minFile = new File(basePath + minPath, cssFile.getName()); minifyCss(basePath, cssFile, minFile); } } } /** * Minify given cssFile to the given minFile and append lastmodified timestamps to CSS * background images relative to the given basePath. * @param basePath The base path of static content. * @param cssFile The CSS file to be minified. * @param minFile The minified CSS file. * @throws IOException If something fails at I/O level. */ public static void minifyCss(String basePath, File cssFile, File minFile) throws IOException { Reader reader = null; Writer writer = null; try { // Read original CSS file. reader = new InputStreamReader(new FileInputStream(cssFile), "UTF-8"); // Minify original CSS file. StringWriter stringWriter = new StringWriter(); new CssCompressor(reader).compress(stringWriter, -1); String line = stringWriter.toString(); // Find all CSS background images. Matcher matcher = Pattern.compile("url\\([\'\"]?([/\\w\\.]*)[\'\"]?\\)").matcher(line); Set<String> imagePaths = new HashSet<String>(); while (matcher.find()) { imagePaths.add(matcher.group(1)); } // Append lastmodified timestamps to CSS background images and replace originals. for (String imagePath : imagePaths) { long lastModified = new File(basePath + imagePath).lastModified() / 1000; line = line.replace(imagePath, imagePath + "?" + lastModified); } // Write minified CSS file. writer = new OutputStreamWriter(new FileOutputStream(minFile), "UTF-8"); writer.write(line); } finally { close(writer); close(reader); } // Dumb sysout, replace by Logger if needed ;) System.out.println("Minifying " + cssFile + " to " + minFile + " succeed!"); } // Helpers ------------------------------------------------------------------------------------ /** * Silently close given resource. Any IOException will be printed to stdout. * This global method can easily be extracted to your "IOUtil" class, if not already exist. * @param resource Resource to be closed. */ private static void close(Closeable resource) { if (resource != null) { try { resource.close(); } catch (IOException e) { e.printStackTrace(); } } } // Main method -------------------------------------------------------------------------------- /** * Just to demonstrate how your batch job thing should use the Minifier. */ public static void main(String... args) throws Exception { String basePath = "C:/Workspace/YourProject/WebContent/WEB-INF"; String cssPath = "/static/css"; String minPath = cssPath + "/min"; Minifier.minifyCss(basePath, cssPath, minPath); } }
Back to top
Gzip Components
This is the third rule of the YSlow's Server category. Yes, that's also a very good point. Gzip is relatively fast and can save up to 70% of the network bandwidth. For static text content you can just use the aforementioned FileServlet article at this blog. For dynamic text content you'll need to configure the application server so that it uses GZIP compression. This is usually explained in the documentation of the application server in question. In case of Apache Tomcat 6.0 you can find it here. You need to extend the <Connector> element in Tomcat/conf/server.xml with a compression attribute which is set to "on". Here's a basic example (note the last attribute):
... <Connector protocol="HTTP/1.1" port="80" redirectPort="8443" connectionTimeout="20000" compression="on" /> ...
That's all! Restart Tomcat and all dynamic response will be Gzipped. And no, this does not affect the aforementioned FileServlet for static content, you can just keep it as is.
Back to top
Configure ETags
This is the fourth rule of the YSlow's Server category. Again a good point and again also covered by the aforementioned FileServlet article at this blog. The ETags are not needed for dynamic content as they are usually not to be cached.
Back to top
Flush the Buffer Early
This is the fifth rule of the YSlow's Server category. Well, that's also a good point. Flushing the response between </head> and <body>. But that's one of the 0,01% cases where in you can't quickly go around a (cough) scriptlet and thus its use is less or more forgiveable.
... </head> response.flushBuffer(); <body> ...
However, in case of Apache Tomcat 6.0 the HTTP connector uses a buffer size of 2KB (2048 bytes) by default which is configureable using the bufferSize attribute. This is generally more than good enough. The average HTML head with the "default" minimum tags (doctype, html, head, meta content type, meta description, base, favicon, CSS file, JS file and title) already accounts 1 up to 1.5KB in size. In any way, in one of my last webapps I have used a slightly modified WhitespaceFilter which removes all whitespace inside the <body> and instantly pre-flushes the stream before the <body>.
Back to top
Use NIO
When your webapplication needs to handle more than around 1.000 concurrent connections, or when your webserver is also used for other purposes than only serving the web, then it's generally better to use non-blocking IO streams instead of blocking IO streams. It scales much better as you don't need one implicitly opened thread per opened IO resource anymore, instead basically all resources are managed by a single thread. This saves the server from a lot of threads and the overhead of controlling them and the exponentially growing performance drop when the amount of concurrent threads (HTTP connections) gets high. You're for performance also not dependent on the amount of available threads anymore, but more on the amount of available heap memory. It can go up to around 20.000 concurrent connections on a single thread instead of around 5.000 concurrent connections on that much threads.
Most decent servers supports NIO, as does Apache Tomcat 6.0 in the HTTP connector. Basically all you need to do is to replace the default protocol attribute of "HTTP/1.1" with "org.apache.coyote.http11.Http11NioProtocol". The Tomcat NIO connector implementation is also known as "Grizzly". In some full fledged Java EE application servers like Sun Glassfish, this is by default turned on.
... <Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="80" redirectPort="8443" connectionTimeout="20000" compression="on" /> ...
That's basically all! Restart Tomcat and now it will use NIO to handle HTTP connections. Only ensure that you give it enough memory (also in the IDE when developing with it). You can start with 512MB, but 1024MB is better.
Back to top
Copyright - No text of this article may be taken over without explicit authorisation. Only the code is free of copyright. You can copy, change and distribute the code freely. Just mentioning this site should be fair.
(C) September 2009, BalusC
5 comments:
Very good stuff!
Thanks
Great!!. I'll try it when we starting to do tunning.
Can you post some tricks of JSF?, I've a problem, I need to do a flexile Form that take components config from DB. I searching in google and I get some examples and tricks, but I cant fix the problen yet.
Thanks!!!
JSF runs on top of JSP/Servlet. You can just apply the same tricks.
Your actual problem is not actually related to "webapplication performance", it's more an architectural/design matter. You don't know how to do it, so there's already nothing to improve.
I can only suggest that JSF just supports "dynamic component creation". Have a look at Application#createComponent(). Good luck.
Thanks for the suggest!
Hi BalusC, thank you for a great post. I read the first two bullets you have and I have couples question I want to ask you. I am using Java EE 6, with JSF mojarra 2.0.3, and glassfish 3.0.1
1. Content Delivery Network
So, your idea is to have secondary domain to server static content. For example, I would upload all my pictures onto photobucket.com to get the absolute URL and then on my JSF page, I would display those absolute URL using h:graphicImage or img tag. This way will be faster, if I load the image from the same domain as my JSF page. Do I understand that correctly? Of course I wont upload my images to photobucket, or should I?
2. Add an Expires or cache-control header
I see the important of using Expire header, the yahoo page said to implements far future expire header like 10 years, do you know how to do that in Glassfish 3.0.1?
For the cache-control header for dynamic content, when I create a filter for Faces Servlet, it considerably degrade the site performance, moving between pages take from 2 seconds to about 6 second
@WebFilter(servletNames={"Faces Servlet"})
public class MyFilter {
private FilterConfig filterConfig = null;
@Override
public void init(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse hsr = (HttpServletResponse) response;
hsr.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
hsr.setHeader("Pragma", "no-cache"); // HTTP 1.0.
hsr.setDateHeader("Expires", 0); // Proxies.
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
Post a Comment