Thursday, January 9, 2014

Smart Clients and Dumb Pipes for Web Apps

After something like 8 years of coding for the web, both in new and legacy projects, across four companies and some personal projects, I've come to a philosophy on web application layout:
If you require that the client has JavaScript, then you may as well embrace the intelligence of your client-side code.  Let your AJAX calls return simple messages, to be converted to a visible result for the user by code that shipped alongside that UI.

If you have a proper build step, then you can easily implement cache-busting URL schemes, so that HTML can depend on a specific version of included scripts and stylesheets.  When builds are deployed atomically (again, a good thing to strive for), then the delivered HTML will eliminate the chances of version skew between the HTML and everything else it includes—JS, CSS, sprite images, and anything else that may be involved.

Now that the delivered code is guaranteed to match the DOM that it was programmed for, all of the knowledge about the DOM should be stored in the client-side resources.  Consider what happens when we want a "refresh comments" link on our blog posts.  One choice would be to re-render the comments sub-template (aka partial) on the server, then ship it down as a blob of HTML, to be essentially pasted into the page for the browser to re-parse.  This method typically uses code like comments_box.innerHTML = response.data; or $('#comments').html(response.data); as the rendering step.

Unfortunately, making the server do the work doesn't hold constant through time: if the server-side code were updated and the comments template affected, the returned HTML may not match the style rules and would then render poorly at best.  If events are connected based on class names to nodes after the new HTML is dumped in the document, and those class names change, the event handlers will also break.

One project I worked on even sent back <script> tags to be evaluated by the browser.  Updating the common library was a tricky, multi-step process because the library had to update to be compatible with both versions of script from the server, then later (after clients would have cached the new dual-version library) the server could be updated and the backwards-compatible version finally removed from the core library.  I could create a cache-busting URL, but I couldn't retroactively bust the cache of everyone already sitting on the page for their entire work day.

On the other hand, if the template lived on the client, the server could return data as a JSON array (or tree if you thread them), filled with objects, each noting author, time, and comment body.  The client then has to convert this message structure into the template, but the template, class names, styles, and everything are the same ones it knows about, because it was shipped with them.  Events continue to be wired properly, and the result of the template process matches the structure of the previously displayed data.

It's been my experience that decoupling the UI from the AJAX messages makes separate evolution  easier, as well.  Adding a field to an AJAX message and having old clients ignore it is practically trivial, instead of dancing with synchronizing dependencies, or shoehorning in a "sometimes-visible" element to the HTML.
It becomes a philosophy of "render everything on the server, or render none of it there," and for an app that uses any AJAX whatsoever, that implies all the rendering should be on the client.  If the server leans toward JSONP style responses, the initial rendering can be almost trivially pushed to the client by inlining the initial data set:

// server program code
$view->assign("js_init_data", json_encode($js_data));

// server template code
<script type="text/javascript">
    initView({{ js_init_data | raw }});
</script>

Although the server is pasting a token in there, it is not actually rendering the UI.  That's left up to the client.

This method does result in a bit more code being delivered up front, but for each AJAX request made, the message size is reduced since it lacks HTML dressing.  It slows down an initial request in exchange for accelerating a (hopefully large) number of more-interactive requests, and for eliminating most chances of users seeing a "weird" page state when the server code is updated.

No comments: