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.