Turbolinks

More like TurboSTINKS

What is Turbolinks

  • Introduced as a default gem in Rails 4
  • Uses AJAX to swap out title and body of page when navigating
  • Provides caching
  • Can speed up page load on JS/CSS heavy sites up to 2x

Normal Page Lifecycle

  • Someone clicks on a link
  • All page context is lost
  • Standard lifecycle callbacks are called (e.g. document ready as used by jQuery)

Turbolinks Page Lifecycle

  • <title> and <body> elements have their contents replaced removing any child elements from the DOM
  • However the JavaScript engine's context is preserved.
  • Since we can't rely on the standard lifecycle alternate callbacks are provided
  • page:before-change — a Turbolinks-enabled link has been clicked
  • page:fetch — starting to fetch a new target page
  • page:receive — the page has been fetched from the server, but not yet parsed
  • page:change — the page has been parsed and changed to the new version
  • page:update — is triggered whenever page:change is PLUS on jQuery's ajaxSucess
  • page:load — is fired at the end of the loading process.

Third Party Scripts

i.e. where this all falls down

  • Third party scripts like Facebook's JS SDK, Google Maps etc. expect the standard page behaviour
  • Most of the time will not reinitialise between Turbolinks page changes

This requires us to hunt down initialisation code in obfuscated, minified scripts in order to hook them into Turbolinks' page:load event.

Even doing that isn't enough.

  • Since JavaScript context is preserved, any objects created in that context are preserved across page changes.
  • This leaves garbage state and dead DOM references for the third party scripts.

This way leads madness

The Turbolinks Compatibility project provides some examples of popular third party scripts made compatible with Turbolinks

Facebook JS SDK

Official code

<div id="fb-root"></div>
<script>
  (function(d, s, id){
    var js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)) return;
    js = d.createElement(s); js.id = id;
    js.src = "//connect.facebook.net/en_US/all.js#xfbml=1";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));
</script>

Facebook JS SDK

Turbolinks compatible code (Coffeescript)

fb_root = null
fb_events_bound = false

$ ->
  loadFacebookSDK()
  bindFacebookEvents() unless fb_events_bound

bindFacebookEvents = ->
  $(document)
    .on('page:fetch', saveFacebookRoot)
    .on('page:change', restoreFacebookRoot)
    .on('page:load', ->
      FB?.XFBML.parse()
    )
  fb_events_bound = true

Continued

saveFacebookRoot = ->
  fb_root = $('#fb-root').detach()

restoreFacebookRoot = ->
  if $('#fb-root').length > 0
    $('#fb-root').replaceWith fb_root
  else
    $('body').append fb_root

loadFacebookSDK = ->
  window.fbAsyncInit = initializeFacebookSDK
  $.getScript("//connect.facebook.net/en_US/all.js#xfbml=1")

Continued

initializeFacebookSDK = ->
  FB.init
    appId     : 'YOUR_APP_ID'
    channelUrl: '//WWW.YOUR_DOMAIN.COM/channel.html'
    status    : true
    cookie    : true
    xfbml     : true

There are still issues even when thinking everything is correctly initialised

Using the example on Turbolinks Compatibility for the ShareThis social media library, Rabid found that URLs were not being detected by the library correctly. ShareThis always thought it was on the page last hard refreshed by the client.

Summary

  • Turbolinks does improve performance of client heavy web apps.
  • A few third party scripts work, with a lot of extra work on the part of the developer.
  • Others just don't or quickly become too maintenance heavy to justify the performance increase.
  • Turbolinks ate my homework, kicked my dog and killed my brother

Good news is, it's easy to get rid of. Remove the references to it from your Gemfile and application.js and you're home free.

Megan Bowra-Dean

megan@rabidtech.co.nz

@megahbite