![]() |
VOOZH | about |
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
The code below is bundled with Rollup. Rollup has determined the bare minimum of imports the code needs to execute with a process known as tree shaking.
๐ Does my bundle look big in this?import React from 'react';
import { useFormikContext, useField } from 'formik';
import isEmpty from 'lodash/isEmpty';
import { DatePicker, Checkbox, CheckboxGroup, Radio, RadioGroup, TransitionComponent, TransitionType } from '@my/component-library';
Tree shaking is a technique used to eliminate dead code. Rollup has determined that I only need two components from formik.
It is no secret that most modern-day JavaScript-heavy applications ship far too much code to the browser.
In the image above, the vast size of react-dom.production.min.js is there for all to see.
formik has needlessly doubled its size by including nearly all of lodash. Lodash is a colossal problem for so many JavaScript bundles.
It seems unlikely that formik is using all the imported modules that are fattening up the bundle size. What is needed is a way of eliminating dead code or only importing the modules that are used by the importee.
In order for a bundler to identify dead code, it will need to perform static analysis on the codebase.
I use TypeScript for everything I can these days, and up until recently, I had the module setting of tsconfig.json set to commonjs.
CommonJS modules are harder to optimize because dynamic imports and exports are supported:
const a = require(localStorage.getItem('somekey'));
const b = 'exportThis';
module.exports[b] = (a) => a;
Because require is actually a function call, we can use a dynamically computed string to determine at runtime the module to be loaded.
A static analyzer will not even attempt to decipher dynamic imports and exports and instead grab everything.
Both import and export statements are part of the language with ESM modules. There is no ambiguity, and the lack of dynamism facilitates static analysis.
Dynamic imports are still possible in ESM modules for code splitting.
I have used Webpack for a long time and Webpack works by wrapping each module in a function that implements a loader and a module cache. At runtime, each of these modules is evaluated in turn to populate the module cache. The Webpack approach makes things like hot module replacement (HMR) possible but incurs overhead with this approach.
Rollup takes a different approach โ it puts all code at the same level, which is known as scope hoisting. The resulting bundle is smaller with much less overhead as there is no per-module evaluation.
The trade-off is that rollup relies on ESM module semantics. Step 1 of any journey to a smaller bundle size is to turn any CommonJS packages into 100% ESM packages.
Bundlers such as rollup or Webpack generally have a mechanism to specify which field in the package.json file is the entry point.
If the consuming package has an import such as:
import * as D3 from 'd3';
The following fields of package.json will determine what the entry point for the module is:
type with a value of module๐ version, module, browser, main fields in packagejson
With Webpack, it is possible to set a precedence of which fields will be searched first using the mainFields option:
resolve: {
mainFields: ['module', 'browser', 'main'],
With Rollup you can use the @rollup/plugin-node-resolve rollup plugin:
resolve({
mainFields: ['module', 'browser', 'main'],
The first step in our journey should be to set the type to module and supply a module field:
{
"name": "@ds/util",
"version": "6.3.0",
"type": "module",
"module": "dist/index",
"browser": "dist/index",
"main": "dist/index",
}
I had problems with Webpack if there was a file extension either module , main or browser and Webpack relied on the existence of a browser field when targeting web builds.
mjs filesES modules are the target for files with an .mjs extension. Later versions of Node assume that an import of an mjs file will be ESM compliant.
In the browser world or more accurately, the bundler world, it is more of a convention, but Webpack and Rollup will treat this file differently and compile to a different target.
I ran into problems with dependencies like React and react-router that are CommonJS dependencies. The solution was to output a file with a .esm.js file extension.
require into touchThe main refactoring that I encountered was to remove require statements that were importing scss files:
const styles = require('./Start.module.scss');
Which becomes:
import styles from './Start.module.scss';
With TypeScript, I had to instruct the compiler to transpile to ESNext.
In tsconfig.json, the module field changed from :
"module": "CommonJS",
to
"module": "ESNext",
This is the Rollup configuration I settled upon:
const bundle = await rollup({
input: inputFile,
external: (id: string) => {
return !id.startsWith('.') && !path.isAbsolute(id);
},
treeshake: {
moduleSideEffects: false,
},
plugins: [
resolve({
mainFields: ['module', 'browser', 'main'],
extensions: ['.mjs', '.esm.js', 'cjs', '.js', '.ts', '.tsx', '.json', '.jsx'],
}),
json(),
postcss({
extract: false,
modules: true,
use: ['sass'],
}),
typescript({
clean: true,
typescript: require('typescript'),
tsconfig: paths.tsConfig,
abortOnError: true,
tsconfigDefaults: {
compilerOptions: {
sourceMap: true,
declaration: true,
target: 'esnext',
jsx: 'react',
},
useTsconfigDeclarationDir: true,
},
tsconfigOverride: {
compilerOptions: {
sourceMap: true,
target: 'esnext',
},
},
}),
babel({
exclude: /\/node_modules\/core-js\//,
babelHelpers: 'runtime',
...babelConfig,
} as RollupBabelInputPluginOptions),
injectProcessEnv({
NODE_ENV: 'production',
}),
sourceMaps(),
minify === true &&
terser({
compress: {
keep_infinity: true,
pure_getters: true,
passes: 10,
},
ecma: 2016,
toplevel: false,
format: {
comments: 'all',
},
}),
],
});
}
The following plugins where used:
Calling rollupโs rollup function with the above configuration returns a bundle object that can write the bundle to disk with the following code:
await bundle.write({
file: path.join(paths.appBuild, 'index.js'),
format: 'esm',
name: packageName,
exports: 'auto',
sourcemap: true,
esModule: true,
interop: 'esModule',
});
The package is now ESM compliant.
The world is turning to ESM modules slowly but surely. There is the promise of a smaller bundle size to lure you to the promised land.
I use Rollup to bundle my packages but still use Webpack for hot module replacement in development.
Whatever the bundler, it is making more and more sense to move away from CommonJS.
Thereโs no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, youโll need more visibility to ensure your users donโt run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
๐ LogRocket Dashboard Free Trial BannerLogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your appโs performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently โ start monitoring for free.
Learn how to use Gemini CLI subagents to delegate frontend, backend, testing, and docs tasks to specialized agents with guardrails and clear ownership.
Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.
Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.
TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension โ no new framework required.
Hey there, want to help make our blog better?
Join LogRocketโs Content Advisory Board. Youโll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now