Simple Lazy Loading jQuery Plugin and Defense of Turbolinks

About a month ago, a big Rails app I’ve been working on made the big switch over to Turbolinks. Being a monstrously JavaScript-infused UI experience, naturally there was code refactoring to be done. There were basically 3 patterns I followed to make it turbo-ready:

#1 – Lightweight and common functions that were used site-wide were declared unobtrusively via event delegation on the document object.

1
$(document).on 'click', '.foo', -> $(@).bar()

#2 – Page/interface-specific initialization functions were bound to custom jQuery events.

publisher.coffee
1
2
3
4
$(document).on 'init:publisher', (e, s) ->
  $scope = if s? then $(s) else $("html")
  return unless $scope.find("#publisher")
  # initialize that publisher

Here, the s argument placeholder allows us to pass in the scope of the DOM we are only interested in initializing as a jQuery selector, i.e. $(document).trigger("init:publisher","#content"). If no argument is supplied, the scope defaults to the entire page. This allows us to trigger these events more conservatively which is particularly useful for custom non-turbolinks ajax callbacks. If the parent element that needs initializing isn’t found within the scope, the function doesn’t waste any more time.

#3 – The page:change event assumed the role of document:ready.

pagechange.coffee
1
2
3
4
5
6
7
# first page load
$ -> $(document).trigger "page:change"

# each turbolink page load
$(document).on "page:change", ->
  $(document).trigger "init:publisher"
  ...

Once scripts were massaged into place, the app was multitudes faster. Most page requests were completed under 300ms and the lack of a loading page flash gave the app a much more native-like experience. The dashboard, however, was still taking too long to load. The dashboard#index controller action was heavy – it had to publisher with tabbed forms for different content types, a stream of recent activity had to be rendered, and a handful of sidebar widgets had to each run their specific database queries.

Since the various sidebar widget queries were the slowest chunk of dashboard#index, as well as the least immediately relevant information on the page, they were the prime candidate to be lazily loaded. I extracted them to their own controller actions that render widget partials and created routes for each of them. Then I wrote this tiny jQuery function to load them after the new page was turbolinked in.

jquery.lazy.coffee
1
2
3
4
5
6
7
8
$.fn.lazy = ->
  @each ->
    $this = $ @
    url = $this.attr('data-lazy-url')
    $.get url, (resp) ->
      $this.replaceWith(resp)

$(document).on "page:change", -> $("[data-lazy-url]").lazy()

On the initial dashboard view, an <div> empty with an attribute of data-lazy-url=<%= specific_widget_url %> holds the place for the widget that is on the way. If you need to update the widget before the next page:change, you can the add data-lazy-url attribute to outermost element of the returned widget and call $("[data-lazy-url]").lazy() whenever you need to. The result was a huge speed boost, cutting the page load time in half, and the widgets were usually in place before the user would even notice them not being there.

There have been mixed feeling on turbolinks since DHH released it. I do think that the “true” future of web applications will follow the course of seeing a tighter relationship between JavaScript app frameworks and JSON API’s, where the UI is managed and built within the browser while only sending data back and forth to the server. But I can vouch for the turbolinks (or pjax) approach being a super boost to a Rails app’s performance and experience. What I don’t accept is the grumping that turbolinks most likely forces you to re-factor your JavaScript code, due to what it grants you for the effort required, especially when compared to the effort of migrating an app to something like Ember. With a nominal bit of effort and consideration, and ideally coupled with good caching and a healthy concurrent server, you can keep rocking with your slim templates, jQuery plugins, and the rest of your bag of Rails tricks without rubbing against the grain of familiar and well-worn conventions.

Comments