async/defer

Author: nateberkopecCreated May 3, 2017Updated Jul 1, 2021
Labelsenhancement

Alright, after I learned that "it turns out events are hard", I'm going to write down all my thoughts about how Turbolinks should work with async/defer and possible solutions:

Expected Behavior

  1. On initial page load, turbolinks:load should always fire.
  2. Most people have hooks on turbolinks:load in their application.js bundles. Whether using async or defer, turbolinks:load should fire after these hooks execute.

Example hook:

//= require turbolinks

document.addEventListener("turbolinks:load", function() {
  console.log("TL load")
});

Actual Behavior

defer

I think Turbolinks basically works "as expected" with defer right now. Since deferred scripts execute before DCL, Turbolinks installs it's hook on DCL correctly, which then fires everyone's turbolinks:load hooks.

async

On initial page load, turbolinks:load only fires if the script downloads and executes prior to DCL, otherwise turbolinks:load is never fired. #274 was intended to fix this, but it only fixed the case where Turbolinks executed after the hook (e.g. in development maybe, or if you just included turbolinks AFTER the hook in your bundle).

Solutions

Since we actually have a story here, it seems pretty simple to set up a real test for. So that would be step one (the solution is more tests, imagine!).

Delay until complete

JQuery has battled a similar-but-not-exactly-the-same problem for nearly a decade. Basically still exists on JQuery 2+ AFAICT. Their proposed solution is basically wait until readyState is complete. This would work for us, but it delays turbolinks:load until all images and other unrelated subresources have downloaded, which would result in really confusing behavior where Turbolinks hooks wouldn't fire until long after the page was ready to use.

Detect if DCL has fired already or not

Throw:

<script>
document.addEventListener('DOMContentLoaded', function() {
  window.DOMLoaded = true;
});
</script>

in the head of the document. This would fix #274's bad interaction with defer, but wouldn't solve the problem for an async application.js bundle which executes after DCL. Also, putting something in the document head is kind of awkward.

Provide a new API for async users

Instead of the example above, do something like:

//= require turbolinks

Turbolinks.ready( function() {
  console.log("TL load")
});

...where ready's contract is to execute all queued functions as soon as the DOM is ready on initial page load or immediately if that has already occurred, and to fire them all again on each navigation. Since Turbolinks knows it own state, it can manage this for the user.

Use requestAnimationFrame

Proposed by @javan

If I understand this option correctly, RAF would delay turbolinks:load until the next frame, which would effectively make it fire after the current script is done executing. I think this would fix async for people with Turbolinks and their handlers inside a single application.js bundle.

EDIT: corrected description of async on 5.0.0 EDIT: added requestAnimationFrame