ES6 has been with us for 2 years. ES2016 and ES2017 are standard practice. ES2018 is just around the corner.

And yet, sometimes you still need JavaScript practices so arcane you’ve almost forgotten they exist. Such was the case with a production bug we discovered after a performance optimization.

Every once in a while, you would refresh our webapp and stare at the loading animation forever. Yes, we have one of those because we’re cool.

We worked hard on it, and it looks great. But we don’t want you to be stuck staring at it never getting to the page.

We traced the problem down to a JavaScript error during app initialization. Sometimes Webpack would try to execute modules before they were ready.

This shows up as a cryptic error πŸ‘‡

Uncaught TypeError: Cannot read property 'call' of undefined
    at __webpack_require__

It happens on this line inside manifest.js πŸ‘‡

// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

Now manifest.js is a generated file, so there isn’t much you can do to fix it. In fact, you shouldn’t have to fix it. Or even consider modifying it. Ever.

Webpack generates this file when you compile your code and uses it to bootstrap your code. It builds a list of modules inside that modules dictionary and executes them when you run import or require().

Usually, this either always works or never works.

It always works when your code is correct. It never works when you forget to export your module.

But it’s never supposed to get into such a situation that it sometimes works and sometimes doesn’t. That is right out.

So I started to dig.

And dig.

And dig some more.

Our bug stymied even grandmaster TheLarkInn.

I dug for an hour. Then two. Then five.

I tried everything. I added console logs into manifest.js to track which module exactly was causing a problem. I used Webpack’s official analyze tool to inspect our builds and associate moduleIds with specific code files.

At up to 3 minutes per compile, it was slow going.

I changed this and that and nothing worked.

Then I found this old article Jake Archibald published in 2013. Deep dive into the murky waters of script loading

In it, he explained that scripts dynamically inserted into the DOM were async by default.

πŸ’‘

Why does that matter? Because our performance optimization made our scripts preload then injected them into the DOM when the files were loaded.

This meant that sometimes our main_code.js would start executing before our main_code_vendor.js file was loaded. As a result, core libraries our code depends on weren’t loaded yet by the time our code tried to use them.

Ha!

Some background

Let me give you some background.

We use Webpack to split our app into multiple files. Libraries go into an app_vendor.js file, and our code goes into an app.js file. Most of our apps also have chunks that Webpack loads asynchronously when they’re needed.

So you need to load at least 3 JavaScript files to make any of our apps work

  1. manifest.js
  2. app_vendor.js
  3. app.js

Loading scripts as async is an old technique to make webapps faster. You write <script src="bla" async></script> and the browser doesn’t wait for JavaScript to load before moving on to rendering the rest of your DOM.

This is great, but it leads to problems. Scripts might execute in random order.

So instead, we used defer for a long time. This downloads scripts without waiting for them, then executes all of them in order as they were defined.

Wonderful.

But preloading is even better πŸ‘‰ <link rel="preload" href="bla" onload="loadJsFiles(this.href) />. With preloading, you’re loading scripts without waiting for them, potentially before the user even opens your site, then executing a callback to say “Ok we got the script, now what?”

In our case, the “now what” part would create a script DOM node and insert it into the page. That makes it execute.

function loadJsFile(file) {
    // keep track of what's been loaded
    if (allScriptsLoaded) {
        loadedScripts.forEach(function () {
            var script = document.createElement('script');
            script.src = '<%= "#{js_file_href}" %>';            document.body.appendChild(script);
        })
    }
}

Keep track of all scripts that were loaded, insert them into the DOM, and trigger their execution when all are ready.

The solution

This “Preload as much as possible, insert into DOM when all is ready” works great.

Except when it doesn’t.

Sometimes they would execute in the wrong order. Our business code would start executing before our vendor code and discover that the libraries it needs weren’t there.

πŸ’©

But why? We wait until all scripts are preloaded before inserting them into the page.

Because dynamically inserted scripts are async by default. You have explicitly disable that with script.async = false.

And everything works.

2013-era JavaScript strikes again. Still relevant.

Learned something new? πŸ’Œ

Join 8,400+ people becoming better Frontend Engineers!

Here's the deal: leave your email and I'll send you an Interactive ES6 Cheatsheet πŸ“– right away.Β  After that you'll get an email once a week with my writings aboutΒ React, JavaScript,Β  andΒ life as an engineer.


You should follow me on twitter, here.