Hacker News new | past | comments | ask | show | jobs | submit login
Managing Async Dependencies with JavaScript (hackcabin.com)
63 points by tnorthcutt on May 19, 2017 | hide | past | web | favorite | 30 comments

If your intent is to load scripts asynchronously and keep their order, just use `<script defer>`. From what I see, you only ever need something like `fetch-inject` if you also want to load stylesheets OR if you want a load callback.

I've been using `defer` exclusively ever since I read this: https://calendar.perfplanet.com/2016/prefer-defer-over-async...

The `defer` attribute waits until the document is finished parsing. Async doesn't wait.

Yes this is true, defer should pull down scripts asynchronously, but execute them in inserted order. If it's just scripts that's all you need to do, you don't need this.

Where are the numbers? What is the speed up?

The effort that goes into these posts without addressing the basics always boggles my mind.

For mobile, my timings showed fetch/XHR to be heaps laggier than <script> tags. This matters for any resources on the critical path for rendering your page.

I naively thought that it would be quicker to have an inline <script> (coming first in the <head>) and have the script request JavaScript resources via XHR (to manage resource dependencies and load failures).

However, the browser decides the priority for resource requests and CPU usage. I found that XHR requests got low priority and wouldn't even get requested until after painting delays.

WRT performance, fetch is a newer standard and still getting ironed out. Browsers have been using external scripts for years and know how to handle them efficiently. A downside to using `script` tags however, is they have to be on the page when the document is loaded, and they block the browser run loop.

What is the difference between XHR-inject and FETCH-inject? Isn't it that the second simply uses the fetch api and is more modern? But everything could be done with XHR too?

Yes, fetch is basically sugar around XHR. This is just XHR injection using a nice API in the library itself (which some browsers still don't support).

From a user's perspective, you could create the exact same experience using vanilla XHR (which is exactly what AMD module loading does with `define`).

> Yes, fetch is basically sugar around XHR.

Believe it or not but currently it's actually the other way around: XHR is defined in terms of fetch:

> XHR is now defined in terms of fetch (see the calls to fetch in XHR's .send()), meaning fetch is lower level than XHR.

Source: https://jakearchibald.com/2015/thats-so-fetch/ and https://xhr.spec.whatwg.org/#the-send%28%29-method

Yes. I don't see anything in its code that can't be done without fetch.

You can use http push (link preload header) or prefetch to make the browser download all dependencies async. Then require the modules sync. when they are needed. The advantages with modules is that they can always be cached! You do not have to bundle or add compile steps. Just use sync. require. Here's an example require script: https://www.webtigerteam.com/webmodules/require.js

Push has its use cases. It has its distinct drawbacks as well. Consider the scenario when you want to lazy-load a script based on a user action so you're not aggressively loading things the user might not need. You may also enjoy: https://www.smashingmagazine.com/2017/04/guide-http2-server-...

It's up to the browser to optimize. What I've discovered so far is that push has low priority and that if a script is required while it's being downloaded (pushed), the browser (chrome) downloads it twice. The browser could however be smart about it and just make the required script top priority and only download it once. The browser could also parse the pushed scripts (chrome currently doesn't) if the cpu is idle. This opens up for modules on the web, like in Node.JS, but without the latency penalty. Also there is no need for the browser to pre-parse the scripts to look for modules when they are already pushed.

> Others may choose to use a bundler to concatenate all of the JavaScript files into one or two packages, and load them asynchronously to avoid these problems, typically leading to a SEO-unfriendly Single-Page Application primed for Front-end SPOF.

Shortsighted as I am, I struggle to see how fetch-inject guards against SPOF?

Having said that, I can see how this library could be useful for projects with no bundler.

It helps guard against SPOF by enabling the application to load in smaller pieces, helping guard against not just SPOF in the front-end but also SPOF in the development pipeline when a left pad makes your bundler stop working.

For those looking for more in depth info, syntax and use cases, here's a link directly to the library https://github.com/jhabdas/fetch-inject

How does this compete with putting a big script tag at the end of the body in the HTML that contains all the code necessary ? What settings can make this way of requiring scripts useful ?

I guess this can be useful, to load extra functionality at run time, in very big single page application.

In a large document, or one with scripts embedded throughout, waiting until the body is finished parsing could take considerably longer than async loading in the head.

If anyone familiar than I am is reading, I'm not sure I understand how fetchInject differs from using

<script src=... async defer>

for all scripts - could anyone explain?

AFAICT using async defer, one cannot express "you need to load X.js and Y.js before running Z.js, and you need to load X.js and A.js before running B.js". It looks like fetchInject allows you to express that type of thing.

But how is this different from RequireJS? I think RequireJS uses XHR and I don't think it supports CSS, but aside from that, what's new? I feel like this is just reinventing AMD.

Yep, this is the race condition that the author was referring to. RequireJs also let's you express these dependencies, but will block behind the cssdom due to the way it injects. Actually, I made a little library which let's you manage dependencies with script async or defer, and can be considered an alternative to the authors (neat) solution. https://github.com/bohdyone/adm.js

Curious question: Can you explain a bit more on "will block behind the cssdom due to the way it injects"

As far as I know, requireJS doesn't do XHR requests and evals. That sounds ugly. I think it dynamically injects a script tag and loads the module. I maybe wrong.

Either way, how's it a blocking one?

So, the only thing that makes fetchInject, differ from requireJS is the non-blocking way. Is my understanding correct?

This article by an engineer at Google explains it pretty well. My own testing has shown the same results. https://www.igvita.com/2014/05/20/script-injected-async-scri...

also wondering about the cssom comment. I can confirm that requirejs works by injecting <script> tags - <script async> to be precise. requirejs' api isn't promise based, but that's mostly an aesthetic concern.

btw you seem to imply that requirejs blocks - what do you mean by that?

This seems to be a lot simpler in scope. RequireJS allows you to essentially do dependency injection, whereas this doesn't handle that. It also uses the fetch API rather than plain XHR as well as using and returning native promises when fetching libraries. I think it's primarily for loading resources from a different server, like a CDN, in arbitrary but definable order, and that's it.

requirejs doesn't natively support css, but it has a pretty easy to use loader api so it'd be easy to add css support and in fact one already exists: https://github.com/guybedford/require-css

lib you linked is a bit crazy, it uses setInterval to guess when a css is properly loaded/applied and delays running dependent code

imho this is sufficient:

    define('css', function () {
            return {
                    load: function (name, parentRequire, onload, config) {
                            console.log('loading css ' + name);
                            const link = document.createElement('link');
                            link.type = 'text/css';
                            link.rel = 'stylesheet';
                            link.href = name + '.css';
                            setTimeout(onload, 0);


    require(['lib/js/jquery-ui.min', 'css!lib/css/jquery-ui.min'], function () {
        // jquery-ui magic goes here

You don't need to guess, you can check document.styleSheets if it contains your file. See: https://curiosity-driven.org/minimal-loader#plugins

Well, a note: async and defer are different and logically incompatible, in terms of load order. (IIRC) defer preserves order whereas async does not. These alter the dependency management strategy entirely.

Applications are open for YC Winter 2020

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