I started this new project called sightread.org to generate music to practice sight reading. (Still early days, it works with rhythms only). I wanted to go for no build process and modern JS and modern HTML.
How modern is modern?
IE8? IE10? In my head when I think "modern" it always brings an image of polyfills, transpilation and so on to make sure my code works "everywhere". But I think this is antiquated thinking. Most people browse with very capable user agents and there's little reason to send subpar (transpiled) code to them. Where I draw the line of "modernity" is Safari 15.3. Just because I want my code to work on my démodé iPhone 8 which can no longer receive updates and is stuck on this browser version.
Turns out what I want to accomplish is perfectly supported, except for the dialog HTML element. For this, I'll use a polyfill. For everything else - raw JavaScript!
Imports
We know CSS @imports are bad for performance because they reduce parallel downloads. The browser fetches a.css, finds out it imports b.css and goes to fetch b.css.
JavaScript is no different. Here's my initial and naïve implementation:
<script src="abcjs-basic-min.js"></script> <script type="module"> import { App } from './app.js'; new App(); </script> Here abcjs-basic-min.js is a library for music notation, I need its window.ABCJS global in my App, so it's loaded synchronously. App imports other modules, which have their own dependencies and so on. But look what's happening:

abcjs, being synchronous, blocks the rest of the downloads. Then app.js is loaded, then the browser discovers its dependencies and loads these as well. Two-step blocking, not cool.
Solution
Step 1: defer abcjs. This way the browser can move on and discover the other scripts. But defer means still execute in order, so App has access to the global ABCJS.
Step 2: import the dependencies. The browser can then discover and load them in parallel, before app.js is loaded.
<script src="abcjs-basic-min.js" defer></script> <script type="module"> import { App } from './app.js'; import './lib.js'; import './rossini.js'; import './abchelpers.js'; new App(); </script> And look at all the parallelization:

Even while abcjs (the largest download) is still pending, everything else is ready:

Also note that customize.js is not even loaded. That's because it's responsible for a dialog and only loaded when the dialog is needed. Fancy dynamic imports, eh?
const { openCustomDialog } = await import('./customize.js'); The only drawback is repetitive import-ing. But is it really that bad having to think for a second if a file is needed for the initial rendering? I personally don't think so.
Update: A test page showing that just removing the defer attribute and making the first script synchronous effectively disables the browsers' preload scanner from discovering the imports. And this is consistent in FF, Chrome, Safari. Thanks for the idea for the follow up demonstration to Robin Marx!
Comments? Find me on BlueSky, Mastodon, LinkedIn, Threads, Twitter




