Last active
December 17, 2015 13:39
-
-
Save jakearchibald/5618497 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
In an HTTP2 world, delivering multiple small individually cachable | |
and executable scripts can be more efficient than one big | |
concatenated script. Can we better prepare for this? Can it be | |
done in a way that works with existing JS files? | |
Imagine a page that had 4 interactive modules, all of which | |
depended on a couple of utility scripts. We could progressively | |
add behaviour to the page by executing the 'action' scripts | |
as soon as they download & the utilities have executed. | |
This means the execution of actions4.js isn't blocked on | |
actions1-3. | |
We still want scripts defined in HTML, so preloaders and scanners | |
pick them up and start the download as soon as possible, without | |
executing JavaScript. | |
--> | |
<!-- | |
In this example, we introduce a 'future' attribute. | |
'future' downloads the scripts as 'async', but does not execute | |
them. Browsers that don't support 'future' will fall back to | |
synchronous in-order execution. | |
--> | |
<script src="utils1.js" class="script-utils-1" future></script> | |
<script src="utils2.js" class="script-utils-2" future></script> | |
<script src="actions1.js" class="script-actions" future></script> | |
<script src="actions2.js" class="script-actions" future></script> | |
<script src="actions3.js" class="script-actions" future></script> | |
<script src="actions4.js" class="script-actions" future></script> | |
<script> | |
if (document.scripts[0].execute) { // simple feature detect | |
// execute() returns a Future which resolves on successful | |
// parse & execution (no errors thrown in initial execution) | |
document.querySelector('.script-utils-1').execute().then(function() { | |
// utils2 depends on utils1, so execute it now | |
return document.querySelector('.script-utils-2').execute(); | |
}).then(function() { | |
// the rest of the scripts depend on utils1 & utils2 | |
// but can run execute in any order | |
return Future.every( | |
toArray(document.querySelector('.script-actions')).map(function(script) { | |
return script.execute(); | |
}); | |
); | |
}).then(function() { | |
// all scripts have executed | |
}); | |
} | |
</script> | |
<!-- | |
This example does the same as above, but entirely declaratively. | |
'depends' downloads the scripts as 'async', but does not execute | |
until all scripts matching the selector(s) have executed. | |
Empty 'depends' will behave exactly as 'async'. | |
Browsers that don't support 'depends' will fall back to | |
synchronous in-order execution. | |
However, this syntax allows for circular dependencies. Also, | |
what happens if I dynamically add a script.script-utils before | |
one of the action scripts has finished downloading? | |
What if I had a img[src=utils1.js], or even div[src=utils1.js] | |
in the document by accident? | |
--> | |
<script src="utils1.js" class="script-utils" depends></script> | |
<script src="utils2.js" class="script-utils" depends="[src=utils1.js]"></script> | |
<script src="actions1.js" class="script-actions" depends=".script-utils"></script> | |
<script src="actions2.js" class="script-actions" depends=".script-utils"></script> | |
<script src="actions3.js" class="script-actions" depends=".script-utils"></script> | |
<script src="actions4.js" class="script-actions" depends=".script-utils"></script> | |
<script depends=".script-actions"> | |
// all scripts have executed | |
</script> |
One feature I like about ControlJS ( http://controljs.org/ ) is that, in addition to being declarative, it manages external & inline script execution order. It doesn't do anything for dependencies.
Circular dependencies are really easy to detect. It is harder how to handle them.
Some options that I can think of:
- Fail and generate a clear error stating that there is a circular dependency. If you don't know how it works, don't use it or just google it. I think we're all grown up, and if you care enough to use this you care enough how to use this properly.
- Break the dependency by loading the first declared of the cycle first, regardless of dependencies. Produce a warning about the cyclic dependency.
- Only allow dependencies on previously declared scripts, and either fail with an error or try to work around it with a warning. This prevents cyclic dependencies, makes it easier for developers to detect them and might improve parsing speed.
I favour failures with errors over trying to do stuff with warnings, but the latter has been the web way of working since the first browsers. In any case, the possibility to screw up is - in my opinion - not a valid reason for not doing it, if it is so easy to get it right.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'd vote for not worrying about dependencies. I tend to think of JavaScript dependencies as something that shouldn't be defined within HTML, rather in some external spot. I think it gives HTML far too much responsibility over how things work. The
then()
approach is flexible enough that you could do it in an external file...though I'm still not sure it's worth it.