Deploy Native JavaScript Modules in Production: Best Practices & Performance Gains

This article explains how modern browsers now support native ES2015 modules, why earlier performance concerns were based on outdated tests, and provides detailed guidance on using Rollup, code‑splitting, dynamic imports, modulepreload, and polyfills to achieve faster, smaller, and future‑proof web applications.

WecTeam
WecTeam
WecTeam
Deploy Native JavaScript Modules in Production: Best Practices & Performance Gains

Common Misunderstandings About Modules

Many developers believe that modules (the ES‑module syntax and loading mechanism) are only suitable for large‑scale production apps. They often cite an old study that claimed loading modules is slower than loading a single bundled script, recommending bundling unless the app has fewer than 100 modules with a shallow dependency tree (depth < 5). That study used unoptimized source files and did not compare optimized module bundles to optimized scripts, making its conclusions incomplete.

Today, bundlers have advanced enough to output ES2015 modules (including static and dynamic imports) that outperform non‑module scripts. The site referenced in the original article has been using native modules in production for several months.

Optimal Bundling Strategy

Bundling always involves trade‑offs between load speed, execution speed, and cacheability. Deploying ES2015 modules directly lets you change small parts of the code without invalidating the entire bundle cache, though it may increase the time needed for a new user to download all modules.

The challenge is to find the right granularity of code splitting that balances loading performance with long‑term caching. Dynamic import‑based splitting is often too coarse for sites with many returning users. Empirical data suggests that loading fewer than 100 modules shows no noticeable performance difference, and loading fewer than 50 files over HTTP/2 also shows little impact.

For best results, split code as finely as possible—down to the level of individual npm packages—until further splitting no longer improves load time.

Package‑Level Code Splitting

Rollup now supports two features that make high‑performance module deployment easy:

Automatic code splitting on dynamic import() (added in v1.0.0).

Programmable manual splitting via the manualChunks option (added in v1.11.0).

Example configuration that groups every module inside node_modules into a file named after its package:

export default {
  input: {
    main: 'src/main.mjs'
  },
  output: {
    dir: 'build',
    format: 'esm',
    entryFileNames: '[name].[hash].mjs'
  },
  manualChunks(id) {
    if (id.includes('node_modules')) {
      const dirs = id.split(path.sep);
      return dirs[dirs.lastIndexOf('node_modules') + 1];
    }
  }
};

The manualChunks function receives a module path and returns a name; modules without a returned name go into the default chunk.

Using this configuration, imports from a package such as lodash-es (e.g., cloneDeep(), debounce(), find()) are bundled together into a file like npm.lodash-es.XXXX.mjs, where XXXX is a hash of the package.

What If You Have Hundreds of npm Dependencies?

Package‑level splitting remains the optimal approach, but if an app imports many different npm packages, the browser may struggle to load all modules efficiently. In that case, group related packages (e.g., react and react-dom) into a shared chunk because they are typically needed together.

Dynamic Import

Native dynamic import() enables lazy loading but requires a fallback for browsers that support modules but not dynamic imports (Edge 16‑18, Firefox 60‑66, Safari 11, Chrome 61‑63). A tiny (~400 bytes) polyfill can provide this functionality.

To use the polyfill, import it and initialize before any dynamic imports:

import dynamicImportPolyfill from 'dynamic-import-polyfill';

dynamicImportPolyfill.initialize({ modulePath: '/modules/' });

Rollup can rename the generated dynamic import function via the output.dynamicImportFunction option, avoiding conflicts with the reserved import keyword.

Efficient Loading of JavaScript Modules

When code‑splitting, pre‑load all modules that will be needed immediately using modulepreload instead of the traditional preload. modulepreload downloads, parses, and compiles modules off the main thread, resulting in faster execution and less main‑thread blocking.

Example preload links for a main module and three npm package chunks:

<link rel="modulepreload" href="/modules/main.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-one.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-two.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-three.XXXX.mjs">

Rollup’s generateBundle hook can automatically build a map of entry chunks to their full dependency lists, which can then be turned into a modulepreload list.

Why Deploy Native Modules?

Smaller Code Size

Modern browsers load native modules without any runtime loader or manifest code, eliminating the overhead of bundler runtimes such as Webpack’s.

Better Pre‑loading

modulepreload

loads and compiles modules off the main thread, leading to faster interaction and less blocking compared to classic preload of script files.

Future‑Proof

Many upcoming web platform features (standard library modules, HTML modules, CSS modules, JSON modules, import maps, shared workers, etc.) are built on top of ES modules. Deploying as native modules ensures compatibility with these innovations.

Supporting Older Browsers

Over 83 % of browsers worldwide natively support JavaScript modules (including dynamic imports). For browsers that support modules but not dynamic imports, use the small polyfill mentioned earlier. For browsers that do not support modules at all, fall back to the classic module/nomodule pattern.

A Real‑World Example

A demo application (hosted on Glitch) implements all the techniques described: Babel/JSX transformation, CommonJS dependencies (React, React‑DOM), CSS handling, asset hashing, code splitting, dynamic imports with polyfill, and module/nomodule fallback. The source is on GitHub, allowing you to fork and build it yourself.

Demo application screenshot
Demo application screenshot

Summary

Deploying native JavaScript modules in production is now practical and offers performance benefits. Follow these steps:

Use a bundler that outputs ES2015 modules.

Split code aggressively, ideally down to individual npm packages.

Pre‑load all static dependencies with modulepreload.

Include a tiny polyfill to support browsers lacking dynamic import().

Use the <script nomodule> fallback for browsers that do not support modules at all.

By adopting these practices, you can achieve smaller bundles, faster loading, and future‑ready web applications.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptWeb DevelopmentCode SplittingRollupESModules
WecTeam
Written by

WecTeam

WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.