Saturday, February 28, 2009

Reducing Page Load Time on Google App Engine Pages With Multiple JavaScripts

While I was creating Conquer-on-Contact I was annoyed to find out the game's initial load times were very high. Some of this is just because App Engine's performance is damn inconsistent from minute-to-minute, but a look at Firebug's Net tab revealed something shocking:

Oh my word Firefox, what are you doing? Serial downloading in 2009? Here is the offending code:

<script type="text/javascript" src="/js/jquery.js"></script>
<script type="text/javascript" src="/js_min/jquery_form.js"></script>
<script type="text/javascript" src="/js_min/jquery_countDown.js"></script>
<script type="text/javascript" src="/js_min/jquery_color.js"></script>
<script type="text/javascript" src="/js_min/jquery_pulse.js"></script>
<script type="text/javascript" src="/js_/conquer-on-contact.js"></script>

After some Googling I found that IE8 loads scripts in parallel automatically but for some reason Firefox 3.0 is still in the dark ages. This MSDN blog entry is helpful, but I don't really want a hacky client-side solution.

What's the best way to solve this, then? Well, we could combine all the javascript files manually, but this makes them a pain to edit. I don't need to be messing around with jquery.js when I am just trying to use the library.

So why not keep them as separate files but combine them server-side when serving? This will waste some cpu but we can minimize that by caching the results for at least an hour. I set it up starting with some automatic javascript minification code written by Austin Chau. Add something like this to Austin's javascript.py (but change the filenames to fit your own javascripts!):

class JsAgg(webapp.RequestHandler):
def get(self):

data = memcache.get(key="JSAGG")

if data is None:
filenames = ["js/jquery.js",
             "js/jquery_form.js",
             "js/jquery_countDown.js",
             "js/jquery_color.js",
             "js/jquery_pulse.js",
             "js/conquer-on-contact.js"]
data = ""
for filename in filenames:
  data += minify(filename) + '\n'
memcache.add(key="JSAGG", value=data, time=memCacheExpire)

self.response.headers['Content-Type'] = 'text/javascript'
self.response.out.write(data)

...

# serving minified/aggregated javascript from mem cache
apps_binding.append(('/js_agg/', JsAgg))

[Download the modified javascript.py]

Now change the JavaScript loader in your html:

<script type="text/javascript" src="/js_agg/"></script>

So what does the page load profile look like now?

Of course we are getting screwed by Google Analytics now but you can see the critical change: We went from about 1.1s to load all the Javascript down to about 250ms.

It might be a good idea to add the other handlers (serve aggregated from disk, don't minify, etc) as well but I leave that as an exercise for you.

Some have suggested it would be better to access JQuery via Google's dedicated CDN. I agree this is a better choice, but only once all the major browsers get their act together as far as script loading is concerned.

5 comments:

  1. Brandon,

    It would actually be ideal to do this as a pre-deploy hook with a tool like YUI compressor because while serving from the memcache is fast, serving static files is much faster.
    ReplyDelete
  2. I agreed with James Levy. Sometimes, but very rare, GAE script would timeout for unknown reasons. And mostly static files serve faster than Python script. You can read the load times from above screenshots. Only jquery.js slower, but I believe that was just a special case and may not happen often.

    However this technique may also be useful for i18n or special need to serve different roles users, e.g. visitors and logged users have different JavaScript scripts versions. Or you are developing new JavaScript, and you don't want users to have hiccups which was due to typos in new JS. Then you can serve different JS based on roles.
    ReplyDelete
  3. Interesting. Thanks for the tips; I will take a look at the YUI compressor and see if I can squeeze some more speed out of this thing.
    ReplyDelete
  4. On my DEV_APPSERVER (Version 1.1.9) I get the following error:
    IOError: [Errno 13] file not accessible

    And on the live APPENGINE I get the following error on the same code:
    IOError: [Errno 2] No such file or directory: '/base/data/home/apps/[app name]/1.331955838904086978/js/jquery.min.js'

    Any ideas???
    ReplyDelete
  5. Static files are not accessible in your application code, they are served separately. Put them elsewhere, like in 'res' directory.
    ReplyDelete