Hacker News new | comments | show | ask | jobs | submit login
Script-injected "async scripts" considered harmful (igvita.com)
235 points by igrigorik on May 20, 2014 | hide | past | web | favorite | 56 comments



+1 for periodic re-examination of best practices.


It's as important for the updated best practice to be taught to novices, and as important for industry leaders (such as Facebook[1]) to update their current code, which reflects the first (outdated best practice) example in the article.

[1]: https://developers.facebook.com/docs/javascript/quickstart/v...


The problem is to make them visible, specially to beginners.


I agree with this, I had no idea about the "good" practice in the first example. Is there any decent place that I can get truly up-to-date practices?


You might argue that the HTML 5 Boilerplate[1] is a good starting place for learning about best practices. The code & accompanying site contain a lot of comments that explain why things are the way they are.

Problem is, of course, that the technologies used by a common page nowadays go far beyond the stuff present in H5B.

[1]: http://html5boilerplate.com/


Meta, but if you're going to write an article named "X considered harmful", then X really has to be as least as harmful as GOTO's prevalence was in 1968. This article is about a little thing that's current between now and now+3y, and that doesn't really hurt anybody when done wrong. Coding a script tag the wrong way is not going to stop the software engineering industry from making fundamental improvements.


Agreed. The "X considered harmful" meme has really got to stop. There are a couple of articles/essays in which that expression really makes sense, but they are few and far between. This one is not one of them. The ones I've read that actually resonated with me have all been written by authorities within their field and regarding subjects of importance for software development as a whole, not niche subjects such as "CSS drop shadows considered harmful", etc.

(A well written article none the less!)


Especially since the original "X considered harmful" was not formulated by the author, Edsger Dijkstra. It was an editorial emphasis provided by none other than Nikalaus Wirth, who had the qualification of having written or having helped to write a long list of significant programming languages. Dijkstra's origin title was the much more humble "A Case Against the Goto Statement." Wirth's formulation provided a gravitas and finality that signalled a strong endorsement of the essay. The important takeaway lesson, in my mind, is that NOT EVEN EDSGER DIJKSTRA would have presented a paper with such a pretentious title. It was the judicious endorsement of a journal's CHIEF EDITOR. And it is quite literally, at this point, a pretentious title.


> that doesn't really hurt anybody

Well, it does hurt conversion and retention rates, if it causes the page to be rendered slowly. Given how many sites don't load anything until javascript has executed, this could have a meaningful impact on someone's bottom line. And that does hurt businesses and their employees.


And even with the most passionate possible argument, it isn't even comparable to GOTO in 1968.


Perhaps it's because I wasn't even alive, let alone programming, in 1968, but I'd be very interested to hear how losing millions of dollars is incomparably better than inconveniencing programmers.


[delete] wiki'd my question.


Very nice article indeed.

One commonly used variant I missed in the article and I'd be interested what it's positive/negative aspects are (if there are differences at all):

    <script>
        var script = document.createElement('script');
        script.src = "http://example.com/awesome-widget.js";
        script.async = true;
        document.getElementsByTagName('head')[0].appendChild(script);
    </script>
(Note the additional async param)


All browsers since FF 3.6 default inserted scripts to async=true

http://mathiasbynens.be/notes/async-analytics-snippet


Just tried it, and it makes no difference. Still waits for the CSS to load before requesting the script.

At least, in Chrome.


It's a noop.


Thank for yet another gem of an article. As a mostly backend developer, I depend a lot on these types of posts to help me keep up with the current front-end best practices. Anything that is on igvita.com I pretty much absorb without questioning due to the consistently high quality (and correctness!) of the posts.


What keeps browsers from running the scripts immediately and only blocking if/when the script actually does access the CSSOM? It seems like an easy latency win.


In order to know that you need to execute it - therein is the problem. One idea we've been kicking around (on Chrome team) is to do "speculative execution" where we create a snapshot and start executing the script, and rollback if you start doing anything crazy with the CSSOM/DOM... As you can imagine, this requires some careful engineering and has its own (long) list of gotchas. No ETA, but definitely something that's on the table.


[deleted]


Yes, and that's what I'm implying. Even if we did speculative execution, that still wouldn't give us the benefits of being preload friendly. We have a better solution that works in all modern (and even old) browsers, and we can now drop the unnecessary baggage of script-injected scripts.


You are reading too much into a title pattern that's used to get eyeballs on the page.


What if you stick this as the first thing in your body:

  <iframe src="/file.js" style="display:none">
And then at the very end of the body tag:

  <script src="/file.js"></script>
Wont that allow us to start a non-blocking fetch of the script early, but then execute it later? I assume it wont be fetched twice. Especially if you send future Expires headers.


If you want to initiate an early fetch, just put the <script async> tag at the top - you won't block DOM construction or block on CSSOM. If you need ordered execution, put your inline script block above CSS and add your logic there (but please keep it lean).


Could affect rendering though, since the browser may treat the js as content to render. Probably should be benchmarked to make any conclusions.


In my experience, anything with display:none is not rendered by the browser and doesn't take up any real resources. I've used this trick with infinite side scrolling slide show type interfaces when I found that beyond about 20 elements, things started slowing to a crawl. Setting offscreen elements to display:none fixed it right up.


"Doesn't take up any real resources" is anything but true. An iframe in particular is basically a new render process, which adds a lot of overhead. Just because you've set "display:none" on an element doesn't mean it's free.


That seems awfully hacky given the availability (and reasonable fallback) of the async option.


Using <script async> you don't know how much of the DOM will have been constructed at the time it executes. Using an iframe you can choose to start loading the script early, but then specify the exact point at which it should be executed. E.g, after a certain div has been placed in the DOM or after the entire body has been constructed, etc. Also, <script async> doesn't work in IE9 or below.


> Using <script async> you don't know how much of the DOM will have been constructed at the time it executes.

If you are including jQuery or similar, there is the option of having your code hook into the "document ready" events. If you are not using something like that then you could pull out what they use and implement that part yourself.

Another option, which is less clean but might be useful if you don't want to wait for all of a load DOM to transfer for instance, is to check for the parts you do need and if they are not yet present set a timer for a few 10s of ms time to check again.

> doesn't work in IE9 or below

I don't suggest holding back on using features because of old IE. Instead make sure your code works in everything but can take advantage of the extra feature in newer browsers. PEople using old IE might get a less than optimal experience, but it'll work, and those using something better get the optimal behaviour.

Of course, this falls down when maintaining backwards compatibility requires noticeable code changes rather than just accepting old browsers won't display things as nicely and/or responsively.


I like Dojo/AMD because when you specify dependencies, you implicitly give an execution order.

Now, that doesn't necessarily help for this kind of "top level" code, but wouldn't it be cool if you could say: "Load this <script> tag asynchronously, but only run it once the following other <script> tags finish"? (You can give <script> tags HTML IDs just like anything else, right?)


You can kind of do that-- the defer attribute of the script tag behaves similarly to async in that the scripts can be downloaded without blocking, but they are supposed execute in the order that they are included in the page (unlike async).

http://stackoverflow.com/a/10731231/480033

However, browser implementation isn't consistent so there's no guarantees really.


I don't understand why the browser treats the script injection case differently than the blocking script tag case, insofar as blocking on CSSOM is concerned. Why doesn't the browser download the script right away and then just hold up executing it while the CSSOM finishes? I can't think of why it waits to download it.


To clarify, both script-injected and regular <script> tags block on CSSOM: there is no difference there. Only the "async" attribute on script (i.e. <script async>) removes the dependency on CSSOM... which is why you should use it.

As for why the browser doesn't download the script-injected scripts: they're hidden inside of the inline script blocks. In order to figure out what you intend to do inside of that block, the browser would have to execute the JavaScript. The behavior that you want (download wait for CSSOM) is exactly what you get when you use regular blocking tags - those are discovered by the preloader and requests are dispatched. That's what the first and second diagram in the blog post illustrate.


Oh I see. It's that it hasn't yet executed the injection itself. Not sure why I missed that, thanks.


Very cool, thanks for revisiting the best practices of yesteryear!

Quick test in FF: seems it blocks either way, but at least schedules the fetch in the async=true case http://i.imgur.com/AmNjhgz.png


I'm using LAB.js, which lets me declare dependencies between scripts, and then handles sequential or parallel retrieval and execution. Not sure how I could easily replace this as suggested in the article, unless I merge all js into one big file?


I'd assume LAB has this covered.

yepnope will load your script as an image away from the DOM so that it's already cached by your browser.


Do these page performance optimizations check out across the modern browsers? Chrome and Firefox tend to implement such things differently. Not sure about Safari, IE11, etc.


Great stuff from Ilya as usual. But this tip is only important when you'd like Javascript execution to happen as fast as possible. The problem with async is that JS executes as soon as it is downloaded, and that may be before the page has fully loaded or the DOM has been built. For cases where your core content is in HTML and JS is only used for progressive enhancement, won't it be better to use defer?

Why isn't defer more popular?


Unfortunately defer is broken in bunch of browsers - doesn't preserve order, etc. As a result, if you need to preserve order, you're better off with the async function queueing pattern.

Also, if you need to wait for DOM/CSSOM, then you can still use an async script and just install an event listener for DomContentLoaded / DomInteractive / etc., when it loads.


The underlying problem is the loading of the CSS is blocking (blocks DOM as well).

However, that problem is complex to workaround.

Using the async flag doesn't fix that problem, and the async flag also means that you need to make sure your scripts can run in any order (which will cause problems unless you are very careful e.g. use a loader that manages dependencies asynchronously).


Another option is to inject the script:

A. At onDOMContentLoaded and B. Inside a setTimeout(func, 0) block

Then the browser's loading indicator will stop before the script has been loaded, executed and all its assets loaded.

Useful for social media widgets, etc.


I'd say this post is mildly hypocritical. You're using two script-injected async scripts, one from Google Tag Manager and another from Disqus!

Which is to say: if it were that harmful, then maybe you could try fixing them yourself and tell us how it goes? It would be great to provide some advice on how to convert a few popular script-injected async scripts from third parties (especially Google Analytics) into non-"harmful" ones.


What's wrong with just putting the script tag before the closing body tag?


If you put a blocking script at the bottom you're still stuck waiting for CSSOM. If you put a script-injected tag at the bottom you're also blocked on CSSOM and your script is not discoverable by the preloader. For more details , see the comparison table in the post.


I'm not talking about a script injected tag, just a normal tag which doesn't block CSSOM. If you put them right before the closing body tag you address both the CSSOM issue and the DOM blocking since the DOM has already been processed.

I understand that async also enables quicker execution of the script. What I don't understand however is why anyone would use script injection when you can just put the script tag at the bottom of the page to avoid DOM blocking.

What am I missing?


Couple this with the fact that you should already be concatenating and minifying your assets, and this seems like a no-brainer.

I suppose you could argue that this approach helps with CDNs, but therein lies another issue of whether or not serving a library from a CDN really buys you anything over just including it with your site's script payload.


FWIW serving libraries from CDN cache buys you and your users and the whole of the internet bandwidth and electricity.


Great - but what if I need to dynamically load some JS?


Then you're "stuck" with a script loader. Make sure your script loader is itself not being blocked on CSSOM, and is not blocking DOM construction.


Does this mean requirejs being harmful?


Yes.


considered by whom? Saying "it is my opinion that ..." of course wouldn't sound that important.


The "X considered harmful" construction has a long history in computer science, going back to Dijkstra's "Go To Statements Considered Harmful" paper. Wikipedia even has a page on it[1].

It can come off a little arrogant, but he's really just following tradition.

[1] - https://en.wikipedia.org/wiki/Considered_harmful


And I think, like goto statements, there's a cost/benefit analysis to be done when making this choice.


"X considered harmful" articles are harmful articles.




Applications are open for YC Winter 2019

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: