The Ajax Experiment

The Ajax Experiment

GlassFish v3 was released today, you can download it here.  Part of the release includes a new version of the Admin Console.  This blog discusses some experimenting done with Ajax in this release.

The Problem...

When we started planning for the GlassFish v3 Admin Console release, we set out to improve our UI design.  One of our goals was to get rid of frames.

Each frame is treated as a separate document.  This means the browser makes a separate request for each page (and the frameset page itself), often leading to loading the same script, css, and other resources multiple times.  When using JS frameworks, and JSF components which require multiple images, and several other resources... the number of files requested from the server can be staggering for an application like the GlassFish v3 Admin Console.  Sure, you can use expires headers and implement other caching techniques which help a ton, but don't entirely solve the problem.

The problem with using frames doesn't end with requesting too many files:

  • JavaScript across frames can be tricky.  Often the same JS file is loaded in multiple "document" objects -- but different instances of it.  You have to be careful when content in one frame is from a different host.  Using JS across "documents" has to be carefully written to ensure the correct object state is read; etc.
  • Printing does not work as expected -- most browsers only print 1 frame (sometimes this is nice, though)
  • No content can span across 2 or more frames -- frames restrict your "canvas" and limit your design.
  • Interactions between frames are hard to write and harder to debug -- for example updating our tree nodes when navigating the main frame.
  • JSF (and some other frameworks) can "forget" about one of your pages if it's not accessed frequently enough (JSF by default remembers the last 15 "pages" you accessed).

So for those reasons, and probably more, we resolved to not use frames in the GlassFish v3 Admin Console.  Next, we thought we'd introduce "menus" and "tagging" to replace our tree -- giving us more real estate and modernizing the application a bit.  However, feedback we got from the community (sarcastic: Thanks guys!) was loud and clear: keep the tree.  So we were now left with very limited development time, a commitment to use our old Look & Feel (header, tree, and content frame), but a decision to move our application into the 21st century by ditching frames.  So thanks to Jason Lee, we quickly whipped up a design leveraging the YUI Layout Manager.  This gave our application the features of frames, without the baggage frames brings with it.

All was good again, right?  Nope.

We had traded one set of problems for new ones, which mostly centered around performance.  At this point we were loading the entire page for every click made in the browser.  While a full browser page loaded much faster than before, each click now required everything to be loaded whereas before just the content frame was loaded.  This meant our giant tree (that we wanted to remove) was haunting us every time we clicked a button / link, in addition to our page header and content area.  Pages were taking 6-10 seconds on my (admittedly slow 4yr-old) laptop.

The Experiment...

Ajax was not new to our team (nor do I expect it is new to most of you reading this blog -- if it is, you're probably reading the wrong blog).  However, we had not used it too extensively in our application in the past (we generated bread crumbs, calculated a server restart message, and refressed tree nodes when server state changed).  To solve our performance problem, we decided to Ajax was our best hope (or going back to frames -- and that was NOT going to happen!).

Here are some of the ideas we initially considered:

  1. Use iframes for the content area
  2. Manipulate the JSF UIComponent tree via a JSF2 Ajax call, and write custom JS to replace the Woodstock JS-based widgets were were using (present in WS 4.3)
  3. Use a hidden iframe for fetching content, use JS to replace the content area with the new content.
  4. Implement more aggressive caching for the tree/header areas and continue to use full page requests.
  5. Perform Ajax GET requests for new stripped-down content pages.

Approach #1 brought back some of the problems with frames (full document object again, duplicates some of the resources, JS across the iframe was complicated again), so we saved #1 as a last resort.  #2 was very complicated -- especially if we had non-JSF content.  While I still think this approach (implemented correctly) is one of the better approaches, it's not the most flexible or simplest approaches -- so we passed on it too.

We did try approach #3.  The advantage of #3 over #1 is that the "page" has no visible frames so the frames problems #1 introduces were eliminated -- well mostly.  When implementing #1, we ran into issues getting the JS copied over correctly and getting inline JS to execute properly.  We were solving those issues, but it became clear that we were working too hard to get a good solution working.  We abandoned this approach before we got all the kinks out.

Approach #4 didn't too promising, so we took on approach #5.  This involved thinking of the 3 former-frame areas -- which I shall call "header", "tree", and "content" -- as separate pages.  According to the browser, however, the three areas are simply 3 different <div>'s with HTML in them.  On the first page requested by the browser, we make use of Facelet's ui:composition concept (albeit via JSFTemplating) to serve a page in a single request composed of all 3 areas.  When the user navigates to a new "page", however, we make an Ajax GET request for the next page with a flag indicating that we don't need everything:


Each of our pages (in this case applications.jsf) uses the Facelets ui:composition to refer to the "default.layout" template.  That template is responsible for deciding whether to send everything... or just the bare minimum.  It does so with its 'template' property of its own ui:composition:

template="/templates/#{pageSession.bare == 'true' ? 'bareLayout.xhtml' : 'treeLayout.xhtml'}"

The bareLayout.xhtml file -- used when the bare=true flag is set -- sends back the bare minimum (it doesn't even send back the required .js files in most cases since those are already present in the browser).  treeLayout.xhtml, as you've already guessed, sends back everything (FYI, we also used to have a "menuLayout.xhtml" file which used a menu system instead of a tree).

Back in the browser, the JavaScript used to handle the Ajax response gets invoked and we replace the old content area with the new stuff.  It would be great if we were done at this point, however, this strategy requires each link (and button click) to be converted into an Ajax request -- so we iterate over the new DOM elements and modify them to make Ajax requests so they too can repeat this process.

Form submits are a slightly more complicated.  We changed each button to invoke JavaScript which submits the data via JSF2 Ajax.  However, JSF2 Ajax expects UIComponents to simply be "refreshed" and if you navigate or redirect a whole new page is shown -- meaning any tree/header stuff that you didn't want to get updated is lost.  So, we had to override the JSF2 server-side behavior by telling JSF that the "PartialRequest" as not a "PartialRequest":


This gave us complete control to return exactly what we wanted from the server.  Although now that we've changed response format and are not really updating UIComponents, but instead a content area which represents a completely new JSF page in many cases -- we now had to replace the JSF2 JavaScript that handled the Ajax response.  We were able to accomplish this by setting a custom function to the "onComplete" property of the "options" object that is passed to the JSF2 "jsf.ajax.request" function.  In our custom function we had to de-queue the JSF2 request to help JSF2 maintain its state correctly (since the default JSF2 JavaScript was not going to be called).  We were then able to swap the content area with the new content fromt the Ajax response.  Phew! :)

The Result...

Instead of pages averaging 6-10 seconds, page requests are closer to 0.2 to 0.5 seconds!!

Fine print: I made a lot of other performance related improvements which may account for a large part of this improvement, and the server itself has increased in speed.  However, the biggest impact was by far was implementing this Ajax strategy.

This blog is titled "The Ajax Experiment" because that's exactly what we did, but also because I don't think we're done learning.  We ended up using Ajax in a very different way than what is proposed by frameworks like JSF2 (for which Jason and I are both EG members), so that makes me wonder how JSF2 should adapt to welcome this type of model.  I know I'm far from the first to try something like this (look at Google Apps: docs, maps, wave, etc.), but having finally experimented with it myself, I think this is the path to the future of the web (but not the destination).  Lets to continue to experiment and see where this leads us...



Post a Comment:
Comments are closed for this entry.



« August 2016