Handling Large Javascript Files With Turbolinks
Despite being released almost 2 years ago, and being automatically included with Rails since 4.0.0, Turbolinks still seems to cause a fair amount of controversy and debate whenever it's mentioned. Heck, even members of the Rails Core Team seem to disagree on the matter:
<blockquote class="twitter-tweet" lang="en">
“If I could remove one thing from Rails… hm… let me think of something less obvious than TurboLinks” —<a href="https://twitter.com/tenderlove">@tenderlove</a></p>— tone phreak (@jacobian) <a href="https://twitter.com/jacobian/statuses/393752420792410112">October 25, 2013</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
Personally, I think Turbolinks is great. We've considered building Screendoor as a single-page Javascript app, but for the near future, we're mostly happy with the old-school server side rendering + Turbolinks on top, which allows us to achieve a decent level of performance without going full-out API + client-side MVC for our apps, which for a shop of our size would be a heavy lift.
One roadblock we started to hit recently was with the size of our compiled Javascript -- see, since Turbolinks only replaces the <body>
of the document, it's recommended to concatenate and minify the Javascript for your entire app (ostensibly using the Rails Asset Pipeline) into one big, bulking .js
file that will only need to be loaded once. However, that file can get big quickly. Like, really big. When I realized that we were serving more than 400kb of Javascript, I decided that even though that file would be skipped over on a Turbolinks page load, loading half a megabyte of Javascript for first-time and/or uncached users had to be pretty painful.
So what's the solution?
The old method of fixing this problem would be to split your Javascript into multiple files and include them conditionally based on where they're needed. So for a page with a WYSIWYG editor, you would include the wysiwyg.js
script, and you could omit that giant ball of crap on pages without. However, with Turbolinks, any additional <script>
tag in the header will trigger a full page reload. So that's out.
Another idea would be to simply load the script asynchronously via $.getScript
. So instead of:
$('textarea').wysiwyg()
You get:
$.getScript('wysiwyg.js', function(){
$('textarea').wysiwyg()
});
But when loading the page again, you'll end up requesting (and executing) the same Javascript again. No good.
My solution? Something I've dubbed requireOnce
. It takes advantage of the persistant Javascript state to store an array of scripts that have been loaded, and if a script has already been loaded, it simply calls the callback immediately. Here it is, in all its glory:
(function(){
var requiredScripts = [];
window.requireOnce = function (path, cb) {
if ($.inArray(path, requiredScripts) == -1) {
requiredScripts.push(path)
$.getScript(path, cb)
} else {
cb()
}
}
})();
Use it like this:
requireOnce('wysiwyg.js', function(){
$('textarea').wysiwyg()
});
I'm curious to hear what everyone thinks, and how others have dealt with this in the past. Turbolinks can be tricky, but with a couple of tricks like this one up our sleeves, it seems to do alright.