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.
Thanks for your article! It works on mobile Safari, but does it work on the android browser too? I cannot verify that.
Thomas
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.
Which class are you extending in your AppPresenter.java
I cannot seem to find the RevealRootContextEvent class in my program
Are you using Gwt-Platform ?
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
Index index is weird, what is your code for you presenter ?
I have just downloaded and added it to eclipse.
thanks
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);
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.
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
Forgot to tell that that action must be unsecured.
You can override isSecured in your Action.
Cheers,
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