On this page
ES modules in brief
ECMAScript modules (ESM) are the standard module system in JavaScript. A module declares what it shares with export and what it needs with import. Imports are static and resolved before code runs, which allows tools to analyse which exports are used. Each module has its own scope, so variables do not leak into the global namespace. Browsers load ES modules natively with script type="module", and Node.js supports them alongside the older CommonJS require system.
// pricing.js
export function finalPrice(price, coupon) { /* ... */ }
export const CURRENCY = "INR";
// checkout.js
import { finalPrice } from "./pricing.js";Named and default exports
Named exports make the exported names explicit and are easier for editors and tools to track during refactoring. Default exports allow importers to choose any name, which can lead to inconsistent naming across a codebase. Many teams prefer named exports for most modules and use default exports only where a framework expects them, such as page components in some frameworks.
What a bundler does
Shipping hundreds of small module files directly to browsers can be slow and does not handle dependencies from npm, TypeScript or JSX on its own. A bundler, such as Vite, webpack, Rollup or esbuild, starts from entry points, follows imports to build a dependency graph, transforms code, removes unused exports and writes optimised output files. In development, tools like Vite serve modules on demand for fast startup, then produce optimised bundles for production.
Tree shaking and side effects
Tree shaking removes exports that are never imported. It works best with ES modules and with code that has no hidden side effects at import time. If a module runs code when imported, such as modifying global objects, the bundler must keep it. Libraries mark side-effect-free files in package.json so bundlers can drop unused parts. Importing a whole library through a single entry that re-exports everything can defeat tree shaking in some setups, so check your bundle when adding large dependencies.
Code splitting
Not every user needs every feature on first load. Dynamic import(), written as import("./chart.js"), tells the bundler to put that module in a separate chunk loaded only when needed. Frameworks often split by route automatically. Split heavy, rarely used features such as charts, editors and admin tools, and avoid splitting so finely that the page makes many small sequential requests.
button.addEventListener("click", async () => {
const { renderChart } = await import("./chart.js");
renderChart(data);
});Inspect what you ship
- Use your bundler's analysis tool or a bundle visualiser to see which packages take the most space.
- Check the size of the initial JavaScript for each important page, compressed.
- Look for duplicate copies of the same library at different versions.
- Replace large dependencies used for one small function with lighter alternatives or a few lines of code.
- Set a size budget in CI so increases are noticed during review.
Checklist
- Code uses ES modules with mostly named exports
- Modules avoid unnecessary side effects at import time
- Heavy, rarely used features are loaded with dynamic import
- Bundle contents are analysed regularly
- A size budget protects initial page load
Share or cite this guide
Innovate with Ajay Editorial (2026-10-07). JavaScript Modules and Bundling Explained for Frontend Developers. https://innovatewithajay.com/articles/javascript-modules-and-bundling-explained
