Volume 2

Q113: ES Modules, CommonJS and Dynamic Imports

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

This question checks whether you understand JavaScript module systems and their production impact. Interviewers want ES modules, CommonJS, live bindings, default exports, named exports, static analysis, tree shaking, circular dependencies, dynamic imports, and code splitting.

Senior answers should connect module choices to bundling, runtime loading, server-side tooling, and frontend performance.

Short Interview Answer

ES modules use static import and export syntax with live bindings, which helps tools analyze dependencies and perform tree shaking. CommonJS uses runtime require and module.exports, which is more dynamic and historically common in Node.js. Dynamic import() loads a module asynchronously and returns a promise, making it useful for code splitting and conditional loading. Modern frontend apps usually prefer ES modules.

Detailed Interview Answer

JavaScript modules organize code into files with explicit dependencies and exports.

ES modules look like this:

import { formatDate } from "./date.js";

export function renderInvoice(invoice) {
  return formatDate(invoice.createdAt);
}

CommonJS looks like this:

const { formatDate } = require("./date");

module.exports = {
  renderInvoice(invoice) {
    return formatDate(invoice.createdAt);
  },
};

Static Structure

ES module imports and exports are static at the top level.

import { sum } from "./math.js";

This helps bundlers understand the dependency graph before running code.

That enables:

tree shaking
code splitting
dependency preloading
faster analysis
better syntax errors

Live Bindings

ES module imports are live bindings, not copied values.

// counter.js
export let count = 0;

export function increment() {
  count += 1;
}
// app.js
import { count, increment } from "./counter.js";

increment();
console.log(count); // 1

The imported binding reflects the current exported value.

CommonJS Runtime Loading

CommonJS require runs at runtime and can be more dynamic.

const adapter = require(`./adapters/${name}`);

This flexibility can make static analysis and tree shaking harder.

Default and Named Exports

Named exports are explicit and easier to refactor.

export function parseUser() {}
export function serializeUser() {}

Default exports can be convenient for components and primary module values.

export default function UserCard() {}

Teams often standardize export style to reduce import inconsistency.

Dynamic Imports

Dynamic import() returns a promise.

async function loadChart() {
  const module = await import("./chart.js");
  module.renderChart();
}

This can defer expensive code until it is needed.

Code Splitting

Frontend bundlers use dynamic imports to split code into separate chunks.

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

The editor code does not need to be part of the initial page bundle.

Circular Dependencies

Circular dependencies happen when modules depend on each other.

a imports b
b imports a

ES modules can handle some cycles because of live bindings, but cycles still make initialization order harder to reason about.

CommonJS cycles can expose partially initialized exports.

Barrel Files

Barrel files re-export from several modules.

export * from "./button";
export * from "./input";

They can improve import ergonomics, but large barrels may accidentally pull too much code into a bundle if tooling or package structure is not careful.

Tree Shaking

Tree shaking removes unused exports from a bundle when the bundler can prove they are unused.

ES modules help because imports and exports are statically analyzable.

Tree shaking can be blocked by:

side effects at module scope
dynamic property access
CommonJS wrappers
incorrect package metadata
large barrel files

Module Scope Side Effects

Code at module scope runs when the module is evaluated.

console.log("loaded");
startGlobalListener();

Side effects make modules harder to tree shake and harder to test.

Prefer explicit initialization for behavior that should not run merely because a module is imported.

Senior Trade-Offs

Use static imports for normal dependencies. Use dynamic imports for expensive or rarely used features. Avoid circular dependencies in architecture boundaries. Be careful with module-level side effects.

Module structure is not just style; it affects startup cost, bundling, and testability.

Code or Design Example

export async function openReportDesigner(reportId) {
  const [{ createDesigner }, { loadReport }] = await Promise.all([
    import("./designer.js"),
    import("./report-api.js"),
  ]);

  const report = await loadReport(reportId);
  return createDesigner(report);
}

This defers report designer code until the user needs it.

Debugging Workflow

When module behavior is confusing:

check static import graph
check circular dependencies
check default vs named import mismatch
check module-level side effects
check whether dynamic import creates a separate chunk
inspect bundle analyzer output
check package ESM and CommonJS fields

Interview Framing

Compare ES modules and CommonJS, then explain live bindings and static analysis. Finish with dynamic imports and code splitting.

Revision Notes

ES modules are statically analyzable and use live bindings. CommonJS is runtime-oriented. Dynamic imports load asynchronously and support code splitting.

Key Takeaways

Modules shape both architecture and performance. Good module boundaries make code easier to load, test, split, and reason about.

Follow-Up Questions

  1. What is the difference between ES modules and CommonJS?
  2. What are live bindings?
  3. Why do ES modules help tree shaking?
  4. What does dynamic import() return?
  5. When should you use dynamic imports?
  6. What is a circular dependency?
  7. Why can CommonJS cycles be risky?
  8. What is a barrel file?
  9. How do module-level side effects affect bundling?
  10. How would you debug a bloated bundle?

How I Would Answer This In A Real Interview

I would say ES modules are static and use live bindings, while CommonJS is runtime-based. Then I would connect static imports to tree shaking, dynamic imports to code splitting, and circular dependencies to initialization risk.