"after" attribute for async scripts


#1

Continuing the discussion from Element Parsing Listeners:

What would be nice for handling async dependencies would be if we could include an after attribute on scripts loaded with async, stating that the script is not to be run until after the script or scripts with the specified id have been run (and/or discovered/created):

<script src="/vendor/aep.js" async id="await-element-polyfill">
<script src="app-logic.js" async after="await-element-polyfill">

The obvious way to polyfill this (to me) is to use an attribute like data-src instead of src on script tags that use after, but this loses out on having the script be potentially pre-loaded by the UA (without cache-warming/ServiceWorker hacks that still can’t implement pre-compilation).

(Edit: actually, if the semantics of after are changed to be defined instead of async, then a polyfill might be able to make this change without requiring changes to the HTML, by tweaking script tags as they’re discovered.)

The alternative (rewriting the script’s content to use an approach like AMD) introduces non-trivial overhead on the behalf of the author (which includes the author’s toolchain), especially in the event that the script can be included with page-defined dependencies (which makes this somewhere between non-trivial and impossible for requiring dependencies from a global CDN).

If “page-defined dependencies” sound far-fetched to you, realize that this is the nature of almost all polyfills - allowing this kind of deferment to be defined by the page allows for async scripts to be written using polyfilled APIs with no changes to the script in the event that the polyfill is replaced/omitted.


#2

Don’t HTML imports do this? If each script is included in a HTML import, and the app-logic import first also imports the polyfill import, then you get dependency resolution, async fetch & parse before execute, and execution order is preserved between scripts.


#3

Async HTML imports (if the UA supports them, and that’s a big if) do let you specify scripts to load synchronously (without blocking parsing or rendering, I think) after async scripts, but that still involves author overhead, and it requires figuring out every script’s location in the dependency tree (which I’m not even sure applies if D requires A and B, E requires B and C, and F requires A and C) instead of just allowing the scripts to process using triggers, and it requires a ton of additional file requests (which, even in an HTTP/2 world where they don’t have much more overhead, likely correspond to a complex asset chain).


#4

IIRC HTML imports default scripts to deferred, so they are non-blocking and preserve execution order. Is that not enough? I think trying to say “async after” is a complicated way of reinventing “defer”, since you’re trying to re-impose execution order. I didn’t necessarily mean that the HTML import itself should be async, either.

Using the default deferred mode of HTML imports has little author overhead AFAICT, since you just put any dependency imports at the top and you’re good. Tools like vulcanize can compile down a bundle of HTML import files in to a single resource, which is similar to the existing widespread practice of minifying code.


#5

From my understanding, the async nature of HTML imports (and by necessity the scripts within them) is controlled by an async attribute on the import’s link tag. See http://www.html5rocks.com/en/tutorials/webcomponents/imports/#performance.

Also, in terms of this being "reinventing defer", the defer attribute’s only purpose, in practice, is to get a buggy async-like behavior in a few versions of IE that didn’t support async (if the browser recognizes async, defer has no effect). Even the effect defer did have didn’t have the same semantics as after (IIRC, defer deferred any script that used it to wait until after DOMContentLoaded). So, yes, maybe this is "reinventing defer" - but defer is due for reinvention.


#6

“defer” preserves execution order (so you don’t need to worry about the random execution order of async scripts) and does not block the rendering of the page, which scripts do by default. That sounds useful to me. Are you sure your disregard for it is warranted?


#7

Yes. When you use defer so as to not “worry about” the problems of the arbitrary execution order (and, crucially, arbitrary execution point / page state) of asynchronously-loaded scripts, you’re also not reaping the most significant benefit of that arbitrary execution: they allow your script to run as soon as it is ready, rather thain waiting for the page to be parsed (which is how defer is specced to work).

Anyway, even if there’s some theoretical subset or stricter semantics for defer that might be able to tackle most of the use cases for after (ie. disregarding the part where the script is deferred until the page is fully parsed), in practice, defer's behavior has been historically poorly-defined by implementations (eg. IE actually did/does disregard the part until waiting until the page is parsed), which is why the generally-agreed upon best-practice around using it is only to give async-like performance in old versions of Internet Explorer - this is also why async is specced to completely override defer by any UA that recognizes async.

If you’re a big believer in some kind of defer-like semantics (and a disbeliever in the many other advantages of after), the biggest advantage of the after attribute that I’m proposing is that it would give authors a way of obtaining that with semantics not broken in browsers from the Bad Old Days before the Living Specification:

<html>
<head>
<script after="page-end" src="deferred.js"></script>
</head>
<body>
<!-- ...insert 5 MB of HTML you simply *have* to parse first here... -->
<script id="page-end"></script>
</body>
</html>