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.

12 comments

  1. Thomas Bachmann · January 16, 2011

    Thanks for your article! It works on mobile Safari, but does it work on the android browser too? I cannot verify that.
    Thomas

    • Brendan Doherty · January 16, 2011

      I believe it should work with any modern browser that will run gwt.

      I’ve just tested it using my Samsung Galaxy Tab which runs Android 2.2. Works fine using the default browser, and Firefox 4 beta 3.

  2. Joseph · March 2, 2011

    Which class are you extending in your AppPresenter.java

    I cannot seem to find the RevealRootContextEvent class in my program

    • Christian Goudreau · March 2, 2011

      Are you using Gwt-Platform ?

      • Joseph · March 2, 2011

        Meanwhile in your line of code

        RevealRootContentEvent.fire(this, this);

        The following error

        The method fire(HasEventBus, Presenter) in the type RevealRootContentEvent is not applicable for the arguments (index, index)

        Any help with that

      • Christian Goudreau · March 3, 2011

        Index index is weird, what is your code for you presenter ?

  3. Joseph · March 2, 2011

    I have just downloaded and added it to eclipse.

    thanks

  4. Anton · March 3, 2011

    Good day,
    I have a problem with my GAE application.
    First of all thank you for good post.

    My problem is caching nocashe.js file. I configure my app by your recommendations.
    Sometimes when users comes first time is all ok, but when it try to enter into application error occures.

    In GAE log i see:

    #
    2011-03-02 12:43:05.790

    com.gwtplatform.dispatch.server.AbstractDispatchServiceImpl cookieMatch: No cookie sent by client in RPC. (Did you forget to bind the security cookie client-side? Or it could be an attack.)

    #
    E 2011-03-02 12:43:05.790

    com.gwtplatform.dispatch.server.AbstractDispatchServiceImpl execute: Cookie provided by RPC doesn't match request cookie, aborting action, possible XSRF attack. (Maybe you forgot to set the security cookie?) While executing action:

    My Client and Server Modules have

    ClientModule

    bindConstant().annotatedWith(SecurityCookie.class).to("GAE_COOKIE");

    ServerModule

    bindConstant().annotatedWith(SecurityCookie.class).to("GAE_COOKIE");

    //XSRF
    filter("*.nocache.js").through(HttpSessionSecurityCookieFilter.class);

    • Christian Goudreau · March 3, 2011

      Usually, the first action that you must send to the server must be insecure, else the cookie isn’t initilized yet.

      That’s not a problem in most of the case, but it seem it is in yours.

      • Anton · March 4, 2011

        Christian,
        Thank you for advice.
        Today i implement it and receive new errors :


        #
        2011-03-04 01:21:40.043

        com.gwtplatform.dispatch.server.AbstractDispatchServiceImpl cookieMatch: No cookie sent by client in RPC. (Did you forget to bind the security cookie client-side? Or it could be an attack.)

        #
        E 2011-03-04 01:21:40.043

        com.gwtplatform.dispatch.server.AbstractDispatchServiceImpl execute: Cookie provided by RPC doesn't match request cookie, aborting action, possible XSRF attack. (Maybe you forgot to set the security cookie?) While executing action

      • Christian Goudreau · March 7, 2011

        Forgot to tell that that action must be unsecured.

        You can override isSecured in your Action.

        Cheers,

    • Brendan · March 7, 2011

      Anton,

      I’m not really sure what the problem is. How about you come discuss it on the gwt-platform google group. Perhaps include some code snipets using something like pastebin.

      Brendan

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s