How I made my GWT/AppEngine application appear to load quicker

It’s a well known “feature” of AppEngine for Java that if your application does not retrieve much traffic, it’ll be unloaded to free resources for other applications. When your application does receive a visitor, your java application must be started. This is referred to as a “cold start”.

Jakob Nielsen’s Usability Engineering guidelines state that for any delay of greater than 1 second feedback is required. A GWT application running on Google AppEngine takes much longer than 1 second to get started. My GWT 2.0 application takes about 7 seconds from cold, or about 4 seconds on a warm AppEngine.

The following describes how I made my AppEngine application appear to load faster, without losing the XSRF protection provided by the gwt-platform framework.

Previously, when I loaded my app, my browser would show a blank page for about 7 seconds while the main html page loaded. To improve the user experience, I wanted to show the user a “fake” progress indicator that makes it seem like something was happening while the application is loading. (Something like every second, % complete += (15% of percentage incomplete). If % complete > 95, then % complete = 50. Gmail used this same pattern when loading. For more discussion on progress indicators, see ajaxpatterns.org.

In my main html file, I moved the script tag that included my .nocache.js from the head, to the bottom of the body.

Above this script tag I added some javascript that renders my progress bar, and use setTimeout to update it every second. The progress bar will continue to update as the javascript is loaded in background, and once my application is ready to be loaded I hide the progress bar in revealInParent() of my top-level presenter. By ordering things this way, my progress bar was displayed first and then the browser made the request to AppEngine to load the .nocache.js file.

Main HTML page

<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>MyApp</title>
  </head>
  <body>
    <div id="loading" style="display: none">
      <div id="progressbar" style="margin: 15px; width: 300px; height: 10px; border: 2px solid black; overflow: hidden;">
      <div id="progress" style="background: silver; height: 100%; width: 0;"></div>
        <script type="text/javascript">
          var loading = document.getElementById('loading');
          var progress = document.getElementById('progress');
          var progressbar = document.getElementById('progressbar');
          function updateProgress() {
            if(loading.style.display !== 'none') {
              var width = parseInt(progress.offsetWidth+((progressbar.offsetWidth-progress.offsetWidth)*.15));
              if(width > (progressbar.offsetWidth * .95)) {
                width = parseInt(progressbar.offsetWidth) * .5;
              }
              progress.style.width = width + 'px';
              window.setTimeout("updateProgress()", 1000);
            }
          }
          document.body.style.margin = 0;
          document.body.style.padding = 0;
          loading.style.display = 'block';
          updateProgress();
        </script>
       </div>
      </div>
      <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position: absolute; width: 0; height: 0; border: 0"></iframe>
      <noscript>
        <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
          Your web browser must have JavaScript enabled in order for this application to display correctly.
        </div>
      </noscript>
    <script type="text/javascript" language="javascript" src="myapp/myapp.nocache.js"></script>
  </body>
</html>

AppPresenter.java

public class AppPresenterImpl extends ...
  // ...
  @Override
  protected void revealInParent() {
    DOM.setStyleAttribute(RootPanel.get("loading").getElement(), "display", "none");
    RevealRootContentEvent.fire(this, this);
  }
// ...
}

To return the main html before the java application starts, the main html file needs to be a static file, as App Engine serves static files from dedicated servers that are separate from the java application servers. Previously I used the code that was suggested in the wiki for XSRF protection, however this would no longer work as my main html file will be static and served from a different server.

DispatchServletModule.java

  • Send cookie to client attached to ¬†.nocache.js

public class DispatchServletModule extends ServletModule {
  @Override
  public void configureServlets() {
    bindConstant().annotatedWith(SecurityCookie.class).to("MYCOOKIE");
    filter("*.nocache.js").through(HttpSessionSecurityCookieFilter.class);
    serve("/myapp/" + ActionImpl.DEFAULT_SERVICE_NAME + "*")
        .with(DispatchServiceImpl.class);
  }
}

appengine-web.xml

  • Aggressively cache index.html and *.cache.*
  • I use gwt-math which puts some javascript files in /myapp/js/, this should be served statically
  • Don’t cache *.nocache.*
  • *.gwt.rpc is required for gwt-rpc serialization
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
  <application>myapp</application>
  <version>...</version>
  <static-files>
    <include path="index.html" expiration="30d" />
    <include path="myapp/**.cache.*" expiration="30d" />
    <include path="myapp/js/*.js" expiration="30d" />
  </static-files>
  <resource-files>
    <include path="myapp/*.gwt.rpc" />
    <include path="myapp/*.nocache.*" />
  </resource-files>
  <precompilation-enabled>true</precompilation-enabled>
</appengine-web-app>

Using a combination of the above techniques has improved my application so that:

  • Users get feedback almost instantly when they first browse to my application (about 0.4s according to Firebug)
  • The security cookie is still provided to the client (Firecookie shows the cookie was sent to the client in the headers and saved when myapp.appspot.com/myapp/myapp.nocache.js was retrieved).
  • Aggressive caching settings means that everything that it makes sense to cache, is cached.