Declaratively loading [better title]
// intro
- We can save on bandwidth
- Declaratively load files we need
The problem
Under the normal flow of the web, when a web browser requests some html from our server, it will immediately start parsing the html content as it finds new tags and starts building the Document Object Model.
When handling external files requested through
<script>
tags, the browser will
synchronously request and load the script tags. That
is to say that the page will be blocked while the JavaScript
external resources is requested. It will look like nothing is
happening on the page and our users will be waiting for the
content to load and be parsed. This waiting time increases the
likelihood that our users will leave and that sucks.
Anytime we use the <script>
tag, our
browser will block the DOM parsing and constructing. For
instance, the following page will load only after the external
resource loads:
<script src="//cdnjs.com/some/library.js"></script>
The delay occurs because the script tag will block the loading of the CSS Object Model (or CSSOM, for short) until after the scripts execute. This feature is so that our JavaScript can reach into our browser's CSS and manipulate it. Our browser waiting to load and parse the css and then it will run and execute our JavaScript.
Additionally, loading our JavaScript in the page forces their execution immediately, even before the page has been created. The browser may already have started to lay out elements on the page while our script manipulates them, which forces the DOM flush and recreate the DOM.
The practice of placing our CSS scripts in the
<head>
tag and our JS at the end of the document avoids some script execution delay by allowing the browser to parse and load the CSS while waiting until the very end of the execution of the page. The page will appear even before the JS is downloaded. However, the JavaScript won't be executed until the page has been created and the CSSOM is ready. For heavy CSS files at the head of the page, even our script-injected scripts will significantly delay the execution of our JavaScript.
We can use the async
attribute with
<script>
tags to tell the browser not to
block the loading and parsing of the DOM/CSSOM in order to
execute the script. Rather than waiting for the browser to
load and parse the CSS to run our JavaScript, it will run it
as soon as it's downloaded.
<script src="//cdnjs.com/some/library.js" async></script>
Awesome, silver bullet, right? Well... kind of not really.
-
async
was introduced in HTML5 and isn't supported by some older browsers, including the Android browser through 2.3 (and of course, Internet Explorer through 9). These older browsers will just ignore the attribute and block the script loading anyway (jerks). -
Ordering isn't preserved with
async
loaded scripts. There is no dependency tree defined because the script will load as soon as it's ready.
Internet Explorer added a defer
attribute to
<script>
tags to try to avoid some of this
waiting WAY back in IE4. It forces the script to be downloaded
in the order they are defined on the page and executed only
after the page has been created.
However, (like lots of Internet Explorer quirks) there are some unfortunate bugs with the implementation from IE4 to IE9. If you're lucky, the script will be executed in order. Not very helpful, eh?
However, in combination, the async
and
defer
attributes can be used in concert to
support browsers that don't cover the
async
spec in HTML.
<script src="//cdnjs.com/some/library.js" async defer></script>
What we want to do is allow our JavaScript to be downloaded along with the rest of our page without blocking rendering and executed in order. There are a few JavaScript libraries that have been developed to try to do this for us, like the magnificant RequireJS. As great as RequireJS is, it requires some significant changes in how we write our JavaScript, and others have CORS issues (despite using clever DOM manipulation tricks). In addition, we have to include RequireJS on the page in order to use it and that requires yet another network call before the page has finished loading. Darn.
A solution
Using JavaScript, we can append our
<script>
tags to the head (after the page
has been loaded) and force the execution order of our
JavaScripts (perhaps using
asynchronous function queuing
or something like
ControlJS).
We can load our scripts without the async tag in JavaScript which forces the scripts to load in the order they are added, while still not blocking the parsing of the DOM.
For instance:
var loadScript = function(src) {
var tag = document.createElement('script');
tag.async = false;
tag.src = src;
document.getElementsByTagName('body').appendChild(tag);
}
loadScript('//cdnjs.com/some/library.js')
loadScript('//cdnjs.com/some/other/library.js')
Using this pattern is supported in all async browsers and maintains the ordering of the JavaScript requests.
All that remains is figuring out one tricky Internet explorer
bug (why does this always seem to be the case?). When
we set tag.src=
above, IE decides it knows enough
information to start downloading the script. We'll have
to handle script loading differently for IE... okay,
let's get to some code.
Solving script-injection
Before we sprinkle React in, let's handle creating a script loading library to properly load and cache our scripts so we can add them to our react component definitions. To do this, let's start off by creating a dynamic cache to handle loading our scripts. Our API will looking something like this:
class ScriptCache {
constructor(scripts) {}
onLoad(success, reject) {}
}
const cache = new ScriptCache([
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-beta1/jquery.js',
'https://cdnjs.com/some/library.js'
]).onLoad(() => {
// everything is loaded after here
})
In order to get our scripts loading, we'll inject a non-async script tag at the end of the document body and handle the callbacks when the script tag loads it's contents. As the entire source of our solution is included with this section, we'll walk through the relevant and interesting parts.
We are using promises with our
ScriptCache
object. If you're unfamiliar
with promises, we suggest you check out the great article on
promises at
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise. The basic points of a promise is that we can handle the
result of an async callback as in a semi-synchronous manner
and that we have the guarantee that they will only be resolved
or rejected once.
When we create a new ScriptCache
object,
we'll want to initiate the loading of our scripts
immediately. We can handle this directly in the constructor,
where we'll create a few usual variables to we'll
want to hold on to throughout the lifetime of our cache
instance.
class ScriptCache {
constructor(scripts) {
this.loaded = [];
this.failed = [];
this.pending = [];
this.load(scripts)
}
// ...
}
In our load()
function, we'll loop through
the list of scripts and create a single script tag for each
one, appending it to the document body and handling both any
errors that may arise from a script load and dealing with
successful states. The load()
function is pretty
straightforward, handling looping through the list. Rather
than building up the script tag itself, it calls out to a
loadSrc()
function to handle loading a single
script tag (this way we can break out each method into smaller
components.)
class ScriptCache {
// ...
loadSrc(src) {
if (this.loaded.indexOf(src) >= 0) {
return Promise.resolve(src);
}
this.pending.push(src);
return this.scriptTag(src)
.then(() => {
// handle success
})
.catch(() => {
// handle cleanup
})
}
}
Lastly, we'll need to define our
scriptTag()
function to create and append a
script tag at the end of the body tag.
class ScriptCache {
// ...
scriptTag(src, cb) {
return new Promise((resolve, reject) => {
let resolved = false,
errored = false,
body = document.getElementsByTagName('body')[0],
tag = document.createElement('script');
tag.type = 'text/javascript';
tag.async = false; // Load in order
const handleCallback = tag.onreadystatechange = function() {
if (resolved) return handleLoad();
if (errored) return handleReject();
const state = tag.readyState;
if (state === 'complete') {
handleLoad()
} else if (state === 'error') {
handleReject()
}
}
const handleLoad = (evt) => {resolved = true;resolve(src);}
const handleReject = (evt) => {errored = true; reject(src) }
tag.addEventListener('load', handleLoad)
tag.addEventListener('error', handleReject);
tag.src = src;
body.appendChild(tag);
return tag;
});
}
}
That's it, for the interesting parts of the
ScriptCache
object. Now, we can use the
<script>
just like it was loaded on the
page with a <script>
tag.
callbacks
Before we leave the ScriptCache
object and move
on to using the darn thing, there is one other
feature that would be nice to include: handling callbacks.
APIs, such as
Google Maps
will call a callback function when the script has loaded.
Since the Google Maps API won't necessarily be ready by
the time the script has loaded, we want to delay our promise
resolution until the actual library has loaded.
For instance, when loading the Google API, our URL we'll load against will look something like this:
https://maps.googleapis.com/maps/api/js?key=apiKey&callback=CALLBACK&libraries=places&v=3.22
The Google Maps API is nice enough to call the function from
above on our window, so after the library has loaded, it will
call window[CALLBACK]()
. Since this is a common
pattern, we can handle this feature as a simple regex replace.
In our loadSrc()
function, just before we set the
src
attribute on the element, let's
substitute the callback value with our own generated function
name that calls directly out to our
handleLoad()
function.
scriptTag(src, cb) {
// ...
if (src.match(/callback=CALLBACK_NAME/)) {
src = src.replace(/(callback=)[^\&]+/, `$1${cbName}`)
cb = window[cbName] = handleLoad;
} else {
tag.addEventListener('load', handleLoad)
}
tag.onreadystatechange = handleCallback;
tag.addEventListener('error', handleReject);
// ...
Notice that we only call
handleLoad
without a callback in the url. We don't want to callhandleLoad()
twice!
The entire source for the ScriptCache
object is
available in the resources section
below.
Adding in React
Can we combine the <script>
tag loading
trick with our React components? Umm... of course we can. That
was a rhetorical question... was that obvious?
With our ScriptCache
class in-hand, we can use it
directly within our react components. We'll look at two
different methods for handling loading our scripts in
programmatically.
componentWillMount()
The most straightforward way to use the
ScriptCache()
object is to build it directly in
our componentWillMount()
lifecycle hook and then
use it in our componentDidMount()
hook.
For example, let's say we want to build a component that
displays the
jQuery-ui datepicker. Rather than needing to mess with our global html and deal
with loading jquery-ui as a global dependency, we can instead
use the ScriptCache
to dynamically and lazily
load it's own dependencies.
If two components require the same js source, the
ScriptCache
won't load another instance on
the page, it will just keep working.
Let's build our Datepicker
component. First,
in our componentWillMount()
function, we'll
create an instance of the ScriptCache
and list
our dependencies:
const Datepicker = React.createClass({
componentWillMount: function() {
this.scriptCache = cache([
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-beta1/jquery.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js'
])
}
});
When the Datepicker
component is getting ready to
mount, the this.scriptCache
instance will lazily
mount our dependencies on the page at the time it needs them.
By the time we get to the mounting time, we'll have the
dependency cache loading. In the
componentDidMount()
lifecycle hook, we can setup
a callback to handle placing the datepicker on the page on the
correct component.
We'll set up our Datepicker
component to
mount a single ref
which we can use when the
component mounts on the page. Since we're defining all of
this functionality inside the
this.scriptCache.onLoad()
function, we can depend
upon jQuery
and jQuery ui
to be
available for us.
const Datepicker = React.createClass({
// ...
componentDidMount: function() {
this.scriptCache.onLoad(() => {
const node = ReactDOM.findDOMNode(this.refs.picker);
$(node).datepicker();
})
},
render: function() {
return (
<div>
<input type='text' ref='picker' />
</div>
)
}
});
Higher order components
Another way we can use the ScriptCache
with React
is by wrapping our components inside of a Higher Order
Component. A Higher Order Component is just a fancy term for
making one component the child of another.
For example, let's say we have a simple
Header
component:
const Header = React.createClass({
render: function() {
return (<h1>{this.props.children}</h1>)
}
})
The Header
component in DOM-land would end up
looking like:
<!-- <Header>This is a header</Header> -->
<h1>This is a header</h1>
If we were to wrap this Header
component in a
Higher Order Component that simply surrounds it in a
container, it's DOM counterpart will look like:
<!--
const WrappedHeader = containerWrapper(Header);
<WrappedHeader>This is a header</WrappedHeader>
-->
<div class="container">
<h1>This is a header</h1>
</div>
Higher Order Components are easy to build as well, they are
simple functions. The containerWrapper
HOC might
look something like:
const containerWrapper = (Component) => {
return React.createClass({
render: function() {
return (
<div className="container">
<Component {...this.props} />
</div>
)
}
})
}
All this to say that to build our HOC, we can simply abstract
what we built with the Datepicker
component and
set the scriptCache handler to be handled inside the HOC
instead of directly in the component.
Moving the cache-specific work into a Higher Order Component
is pretty straightforward. We'll even go the extra step
and add a bit of syntactic sugar where we'll add an
onLoad()
function to pass as a prop down to our
component that can call the
scriptCache.onLoad()
function for us. The
component below never needs to know how it's
getting it's dependencies, just that it does get them.
export const Wrapper = (scripts) => (Component) => {
const wrapperComponent = React.createClass({
componentWillMount: function() {
this.scriptCache = cache(scripts);
},
onLoad: function(cb, reject) {
this.scriptCache.onLoad(cb, reject)
},
render: function() {
return (
<Component
{...this.props}
onLoad={this.onLoad} />
)
}
});
return wrapperComponent;
}
Not exactly complex, eh? Now, rewriting our
Datepicker
from above to use our new HOC
simplifies it even more.
const WrappingDatepicker = React.createClass({
componentDidMount: function() {
this.props.onLoad(() => {
const node = ReactDOM.findDOMNode(this.refs.picker);
$(node).datepicker();
}, (err) => {
console.log('there was an error', err, window.$)
})
},
render: function() {
return (
<div>
<input type='text' ref='picker' />
</div>
)
}
})
export const WrappedDatepicker = Wrapper([
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-beta1/jquery.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js'
])(WrappingDatepicker)
Dynamically and lazily loading scripts is not exactly a revolutionary problem (we've been doing it for years), so there are plenty of fantastic production-ready solutions out there, such as:
With the knowledge of building our own, we can get a better, deeper understanding of how the browser works and how to master async script loading with React.