If you have ever bounced between import and require, or puzzled over .mjs and .cjs files, you are not alone. Modern JavaScript projects often juggle bundlers, Node versions, and browser targets, which makes module formats feel more complicated than they need to be.
Hi folks,
I keep seeing ESM mentioned in docs and configs and I am confused about when to use it. What is ESM in JavaScript? How is it different from CommonJS? I am building a full-stack app and want to use imports in both the browser and Node.
What are the best practices for setting up ESM, handling dynamic imports, and interop with CommonJS modules? Any tips on pitfalls to avoid would be appreciated.
ESM stands for ECMAScript Modules. It is the standardized module system for JavaScript, supported natively by modern browsers and Node. ESM uses the import and export keywords, enables static analysis for better tree-shaking and performance, and encourages clearer dependency graphs.
In contrast, CommonJS is an older effort to create a standardized module system, but primarily for server-side JavaScript. These are synchronous and can be used by ESM, but ESM modulus can’t consume CommonJS ones.
- Syntax:
importandexportinstead ofrequireandmodule.exports. - Static structure: Imports are hoisted and must be at the top level, which enables tree-shaking.
- Top-level await: Supported in ESM modules in modern environments.
- Strict mode: ESM is always in strict mode.
Modern browsers can load ESM directly using a script tag with type=”module”.
<script type="module">
import { greet } from './utils.js';
greet('world');
</script>Code language: HTML, XML (xml)
You can also use dynamic imports to lazy-load code:
// Lazy-load a heavy module only when needed
const { heavyFeature } = await import('./heavy.js');
heavyFeature();Code language: JavaScript (javascript)
Node supports ESM in two main ways:
- Set
"type": "module"in package.json, then .js files are treated as ESM. - Use the .mjs extension for ESM files regardless of package.json.
// package.json
{
"type": "module"
}
// src/main.js
import fs from 'node:fs/promises';
import { greet } from './utils.js';
console.log(await fs.readFile('./README.md', 'utf8'));
greet('Node ESM');Code language: JavaScript (javascript)
Sometimes you must mix ESM and CJS.
- Importing CJS from ESM: often use dynamic import or createRequire.
// ESM file that needs a CJS module
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cjsLib = require('./legacy.cjs'); // CommonJS module
cjsLib.run();Code language: JavaScript (javascript)
- Importing ESM from CJS: prefer dynamic import.
// CommonJS file
(async () => {
const { greet } = await import('./utils.js');
greet('from CJS');
})();Code language: JavaScript (javascript)
- File extensions: In ESM you typically must include the .js extension in relative imports.
- Default vs named exports: Importing default requires a different syntax than named exports.
__dirnameand__filename: Not available in ESM. Useimport.meta.urlwith URL APIs.- JSON import: Use
importwith an assertion in newer runtimes.
import config from './config.json' assert { type: 'json' };Code language: JavaScript (javascript)
- Prefer ESM-ready packages to enable tree-shaking and smaller bundles.
- Use dynamic imports for route-level code splitting.
- Keep assets cacheable and served from a CDN. If you are serving images and videos, review how image hosting works for performance and caching.
// utils.js
export function greet(name) {
console.log(`Hello, ${name}!`);
}
export const VERSION = '1.0.0';
// app.js
import { greet, VERSION } from './utils.js';
greet('ESM');
console.log(VERSION);Code language: JavaScript (javascript)
After you set up ESM in your app, you can import modern SDKs directly and keep your bundle lean. For example, in a front-end project you can use Cloudinary’s ESM-friendly URL Generation library to build optimized image URLs at runtime, then let your bundler tree-shake unused parts.
// In the browser with ESM
import { Cloudinary } from '@cloudinary/url-gen';
import { fill } from '@cloudinary/url-gen/actions/resize';
import { format } from '@cloudinary/url-gen/actions/delivery';
const cld = new Cloudinary({ cloud: { cloudName: 'demo' }});
const img = cld.image('samples/landscapes/nature')
.resize(fill().width(600).height(400))
.delivery(format('auto'));
document.getElementById('hero').src = img.toURL();Code language: JavaScript (javascript)
This pattern plays nicely with CDNs and clean image URLs, and you can choose efficient formats for the client.
- ESM is the standard module system with import and export, supported in browsers and Node.
- It enables static analysis, tree-shaking, and top-level await for cleaner, faster builds.
- Use “type”: “module” or .mjs in Node, script type=”module” in the browser, and dynamic imports for code splitting.
- Interop with CommonJS via dynamic import or createRequire where needed.
- Combine ESM with a CDN-friendly asset workflow and optimized image URLs to minimize payloads and improve performance.
- PNG to WebP Converter
- Image to JPG Converter
- Video as a Service – Guide
- Image Upscaling and Quality Enhancement
Ready to streamline media in your ESM apps? Sign up for Cloudinary free to optimize, transform, and deliver images and video at scale.