Publish, Ship, and Install Modern JavaScript for Faster Applications
The article explains why adopting modern JavaScript (ES2017) and publishing packages with the new exports field, combined with proper bundler configurations such as Webpack or Rollup, dramatically reduces bundle size and improves performance while still supporting legacy browsers when needed.
More than 90% of browsers can run modern JavaScript, yet legacy code remains a major cause of web performance problems. Modern JavaScript refers to syntax supported by all current browsers, including classes, arrow functions, generators, block scoping, destructuring, rest/spread, object shorthand, and async/await (ES2017).
Legacy JavaScript avoids these features, often requiring a compilation step that increases bundle size by about 20% and adds runtime overhead, especially when polyfills and helpers are included.
Modern JavaScript on npm
Node.js now supports an exports field in package.json to declare a module’s entry point, indicating the package requires Node 12.8+ (ES2019) and can contain modern code.
{
"exports": "./index.js"
}Using only the exports field signals that consumers must handle any necessary transpilation.
Publish Modern Code Only
{
"name": "foo",
"exports": "./modern.js"
}Note: This approach assumes every consumer’s build system can transpile dependencies, which is not always true.
Modern Code + Legacy Compatibility
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs"
}Modern Code + Legacy Compatibility + ESM Optimisation
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs",
"module": "./module.js"
}Bundlers like Webpack and Rollup can use the module field for tree‑shaking while still providing a legacy bundle.
Bundler Configuration
Webpack
Webpack 5 can target modern syntax:
module.exports = {
target: ['web', 'es2017'],
};It can also output an ES module bundle:
module.exports = {
target: ['web', 'es2017'],
output: { module: true },
experiments: { outputModule: true },
};Optimize Plugin
Transforms a modern bundle back to legacy code without re‑processing source files, allowing a single source to serve both modern and legacy browsers.
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
plugins: [new OptimizePlugin()],
};BabelEsmPlugin
Works with @babel/preset‑env to generate a modern bundle for browsers that support ES modules.
// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');
module.exports = {
module: {
rules: [{ test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } }]
},
plugins: [new BabelEsmPlugin()],
};Transpiling node_modules
When modern dependencies expose an exports field, they can be automatically transpiled:
// webpack.config.js
module.exports = {
module: {
rules: [
// first‑party code
{ test: /\.js$/i, loader: 'babel-loader', exclude: /node_modules/ },
// modern dependencies
{
test: /\.js$/i,
include(file) {
let dir = file.match(/^.*[\/\\]node_modules[\/\\](@.*?[\/\\])?.*?[\/\\]/);
try { return dir && !!require(dir[0] + 'package.json').exports; } catch (e) {}
},
use: { loader: 'babel-loader', options: { babelrc: false, configFile: false, presets: ['@babel/preset-env'] } }
}
]
}
};Minifiers must be configured for modern syntax, e.g., { ecma: 2017 } for Terser.
Rollup
Rollup can generate both modern and legacy bundles in a single build.
// rollup.config.js
import { getBabelOutputPlugin } from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: [
// modern bundle
{ format: 'es', plugins: [getBabelOutputPlugin({ presets: [['@babel/preset-env', { targets: { esmodules: true }, bugfixes: true, loose: true }]] })] },
// legacy bundle
{ format: 'amd', entryFileNames: '[name].legacy.js', chunkFileNames: '[name]-[hash].legacy.js', plugins: [getBabelOutputPlugin({ presets: ['@babel/preset-env'] })] }
]
};Other Tools
Higher‑level bundlers such as Parcel, Snowpack, Vite, and WMR assume dependencies may contain modern syntax and handle transpilation automatically. Devolution can add legacy fallbacks to any build output.
Conclusion
ES2017 is currently the most widely supported modern JavaScript version. By using tools like npm, Babel, Webpack, and Rollup with the appropriate configuration, developers can publish modern code while still providing legacy bundles for older browsers.
ByteDance Web Infra
ByteDance Web Infra team, focused on delivering excellent technical solutions, building an open tech ecosystem, and advancing front-end technology within the company and the industry | The best way to predict the future is to create it
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.