CommonJS vs ModuleJS: A Detailed Comparison of Their Functionality

Ayushya JaiswalAyushya Jaiswal
6 min read

What is CommonJS?

CommonJS is a module system used in Node.js to organize and reuse code across files. It allows us to import and export functionalities like functions, objects, or variables using simple syntax.

Node.js adopted this module system so developers could:

  • Break code into reusable modules/functions.

  • Load only what is required.

  • Avoid polluting the global scope.

So, instead of writing one giant file, we can use this system to split logic’s across files and link them with each other using ‘require( )’ and ‘module.exports’.

CommonJS was originally built to bring modularity to server-side JavaScript, which was initially missing in the JavaScript used in the browser.

How CommonJS works in Node.js?

Whenever we execute a script inside Node.js using the command node file_name.js, Node.js internally wraps the entire script inside a function called the 'Module Wrapper Function'. This wrapper function is an IIFE and takes several parameters as input, including exports, require, module, __filename, and __dirname. These parameters are injected at runtime into our modules, which help us maintain modularity in our code by exporting functions, variables, and objects from the script and importing the required elements from another script.

🔗
To understand in detail how CommonJS works in Node.js, follow the link below: Understanding Node.js internals

Module Caching

Node caches modules after the first load, meaning:

const logger1 = require('./logger');
const logger2 = require('./logger');

console.log(logger1 === logger2);    // true

This caching technique helps Node.js improve its performance but can sometimes be tricky.

What is ES Modules (ModuleJS)?

ES Modules (ESM) is the official JavaScript module system introduced in ECMAScript 2015 (ES6). It's now fully supported in modern browsers and Node.js.

In simple terms, it lets us:

  • Split our code into reusable files.

  • Use the import and export syntax for exporting functions, variables and objects from the file and import the required elements from another file.

  • Load modules asynchronously (great for performance)

    Asynchronous loading means ESM doesn’t wait for one import of the module to complete its operation before loading another module. All imports load the modules in parallel.

Why ES Modules were introduced

Before ESM, developers used:

  • CommonJS (Node.js)

  • AMD/RequireJS (Browser)

All of these were non-standard solutions with different syntax and behavior.

So, JavaScript needed a standard, cross-platform module system that worked:

  • On the browser and server

  • With static analysis and tree-shaking

  • With better performance and security

Hence, ES Modules have been introduced.

How Does Syntax Differ from CommonJS?

Export:

// CommonJS Syntax
exports.add = (a, b) => a + b;
// OR
const sub = (a, b) => a - b;
const mul = (a, b) => a * b;
module.exports = {
    sub, mul
};

// ES Module Syntax
export function add(a, b) => a + b;
// OR
const sub = (a, b) => a - b;
const mul = (a, b) => a * b;
export { sub, mul };

Import:

// CommonJS Syntax
const math = require('./math');

// ES Module Syntax
import math from './math';

How does ES Modules System Work?

Now the biggest question that arises is that, in CommonJS, functions like export and require are provided dynamically via the module wrapper function to load and export elements from the script. Does a similar case apply in ModuleJS as well?

The answer is NO. Let's understand in detail.

In ES Modules, there’s no wrapper function like in CommonJS. ES Modules are designed to be lexically scoped and statically analyzable, which means that everything is known at compile time.

  • What we’re importing.

  • What we’re exporting.

  • And no dynamic operations are required.

1. Parsed Statically

When Node.js or a browser loads an ESM file, it doesn’t just run it line by line like CommonJS. Instead:

  • It parses the file first (without executing it).

  • Builds a module dependency graph.

  • Resolves all imports before any code runs.

  • Then links everything together and executes.

This makes ESM very predictable and ideal for tree-shaking, preloading, and parallel fetching.

2. Module Scope (not function scope)

Every ES Module runs in its own module scope (not global, not function-scoped), and the language provides new keywords: import and export.

import and export are part of the language syntax—not runtime expressions, unlike what we have in CommonJS.

Example:

// math.js
export const add = (a, b) => a + b;

// app.js
import { add } from './math.js';
console.log(add(2, 3));     // 5

3. How to use __filename and __dirname?

These are Node-specific CommonJS features. To use them in ESM, we can use the code below:

// ESM equivalent for __filename & __dirname
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Module Caching in ESM?

Yes, just like in CommonJS, ESM modules are also cached after the first load in Node.js.

Module Dependency Graph

A module dependency graph is a directed graph that represents how modules depend on each other in a project.

  • Each node in the graph is one module/file.

  • Each edge(arrow) — an import dependency from one module to another.

Consider we have a main.js which imports utils.js, and math.js which internally imports constants.js, then our dependency graph looks like below:

// Module representation
main.js
├── utils.js
└── math.js
    └── constants.js

// Graph representation
main.js
  ↓        ↓
utils.js  math.js
              ↓
         constants.js

Module dependency graph enables:

  • Faster execution (no need to evaluate dependencies at runtime)

  • Tree-shaking (removing unused modules)

  • Parallel loading

  • Better error detection (like circular imports or missing modules)

Tree-shaking

Tree-shaking is a process used during build time (not runtime) to remove unused code from the final JavaScript bundle.

Why is it called Tree-Shaking?

  • Our module dependency graph looks like a tree.

  • Every import/export becomes a branch of that tree.

  • If a branch (like a function, object, class, or constant) isn’t used, it’s shaken off during the build process.

How does ESM perform Tree-Shaking?

ESM is statically analyzable:

  • All imports/exports are known at the compile time.

  • No dynamic loading of the modules.

  • This makes it easy for bundlers to see what needs to be kept for use and what needs to be removed.

Parallel Fetching

Parallel fetching means that when multiple modules are imported in ESM, the JavaScript engine starts fetching all the dependencies at the same time, instead of waiting for one to finish before starting the next.

Why is it possible in ESM?

Parallel fetching is possible in ESM because it has static imports, meaning the engine knows upfront what files it needs. So it can:

  • Parse the top-level ESM file.

  • Immediately kick off fetches for all imported modules in parallel.

  • Wait until all modules (and their dependencies) are loaded.

  • Execute them in the correct dependency order.

Whereas in CommonJS, the first require() must finish completely before moving to the next require().

Difference between CommonJS and ESM

FeatureCommonJSModuleJS (ESM)
Wrapped by function?YesNo
Module ScopeYes (function-based)Yes (native module scope)
Parameters injected?exports, require, etc.No (native syntax like import, export)
When is code resolved?At runtimeAt compile time
Export/Import stylemodule.exports, require()export, import
FlexibilityHigh (dynamic imports possible easily)Stricter, but enables better optimization.
0
Subscribe to my newsletter

Read articles from Ayushya Jaiswal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ayushya Jaiswal
Ayushya Jaiswal