16. Modules: CommonJS vs ES Modules, bundlers, dynamic import()
Two module systems coexist in the JS ecosystem:
// CommonJS (Node's original system, synchronous, still everywhere in npm packages)
const fs = require("node:fs");
module.exports = { readConfig };
// ES Modules (the language standard, async-capable, works natively in browsers)
import fs from "node:fs";
export function readConfig() { /* ... */ }
export default class Config { /* ... */ }
Key differences: ESM is statically analyzable (imports/exports are determined before execution, enabling tree-shaking — bundlers can delete unused exports), is always strict mode, and has live bindings (an imported value updates if the exporting module changes it, unlike CommonJS's snapshot-at-require-time copy).
Dynamic import() returns a Promise and is the standard way to code-split
— load a module only when it's actually needed:
button.addEventListener("click", async () => {
const { openEditor } = await import("./editor.js"); // fetched only on first click
openEditor();
});
Bundlers (Webpack, esbuild, Vite/Rollup) exist because: browsers didn't
support ESM until ~2018 (Webpack predates browser ESM), node_modules has
thousands of small files that are faster to serve as fewer bundles, and
bundlers do tree-shaking, minification, and transform newer syntax down for
older targets.