Networking and resource loading

How module scripts load

Understand the default deferred behavior of module scripts and how their dependency graph changes loading.

A module script is a script tag with type="module". It differs from a classic script in one important way: it’s deferred by default.

<script type="module" src="/app.js"></script>

The browser keeps parsing the HTML while it fetches app.js. Then it reads the static import statements inside it and fetches those files too, and the files they import, until the whole dependency graph is downloaded. Only then does it evaluate the entry module, after the document has finished parsing.

Adding defer to a module script changes nothing. It’s already deferred.

The graph is the cost

The entry file can be tiny and still cause a lot of requests. Say app.js looks like this:

import { renderCart } from './cart.js'
import { formatPrice } from './money.js'

renderCart(document.querySelector('#cart'), formatPrice)

The browser now needs cart.js and money.js before app.js can run. If cart.js imports three more files, those are needed too. Each level of the chain is a round trip the entry module waits for.

Deep chains are the typical performance problem with unbundled modules. Each download is fast, but they happen one level at a time, because the browser discovers the next level only after parsing the previous one.

Dynamic import

import() as a function call works differently:

button.addEventListener('click', async () => {
  const { openEditor } = await import('./editor.js')
  openEditor()
})

The browser discovers editor.js only when execution reaches that line. This cuts the initial work, since the editor code isn’t downloaded until someone clicks. The trade-off is that the feature is slower the first time it’s used.

Trace it in DevTools

Open the Network panel and use the Initiator column to follow module requests. Each import points back to the module that requested it, so you can read the chain top to bottom.

Then record a load in the Performance panel. Compare when a module finished downloading with when it was evaluated. Network completion doesn’t mean the main thread was ready to run the code. If a long task was in the way, the module sat there waiting.

Try this on your own project: create an entry module that imports two small files, reload, and trace the dependency graph in DevTools.

Lesson completed