Contents
Introduction
In frontend applications, bundle size directly impacts user experience. Larger bundles lead to longer initial loading times, and the difference becomes even more noticeable on mobile devices. In this post, I share the experience of reducing bundle size from 547KB to 97KB, approximately 82%, by changing lodash’s import method in a monorepo environment.
Discovering the Problem
Our project is a Vite-based monorepo containing multiple apps. During a code review, we discovered the possibility of reducing bundle size through tree shaking. To verify this, we first set up a bundle analysis environment.
Installing the Bundle Analysis Tool
We installed rollup-plugin-visualizer as a devDependency for bundle visualization. Since Vite uses Rollup internally, we can easily add bundle visualization by leveraging Rollup plugins.
yarn add -D rollup-plugin-visualizer
Adding the Visualizer Plugin to Vite Config
We conditionally added the visualizer plugin to each app’s vite.config.ts so that it only activates when the ANALYZE environment variable is set.
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
react(),
...(process.env.ANALYZE
? [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}),
]
: []),
],
});
Here is what each option means:
| Option | Description |
|---|---|
filename | Outputs the analysis result as dist/stats.html |
open | Automatically opens in browser after build |
gzipSize | Shows size after gzip compression |
brotliSize | Shows size after brotli compression |
Adding an Analysis Build Script
We added an analysis build command to each app’s package.json.
{
"scripts": {
"build:analyze": "ANALYZE=true yarn build"
}
}
Running yarn build:analyze performs a regular production build, then generates dist/stats.html which automatically opens in the browser showing the bundle visualization as a treemap. This allows you to see at a glance how much each library occupies in the bundle.
Analysis Results
The analysis revealed that the lodash library accounted for a significant portion of each app’s bundle.
The cause was simple. Throughout the codebase, lodash was being used with Named Imports as shown below.
import { cloneDeep } from 'lodash';
import { isEqual } from 'lodash';
This approach causes the entire lodash library (approximately 547KB) to be included in the bundle, even if only a single function like cloneDeep is actually used.
What is Tree Shaking?
Tree Shaking is an optimization technique where bundlers (Webpack, Vite/Rollup, etc.) remove unused code during the build process. Just as shaking a tree causes dead leaves to fall, it “shakes off” unused code.
However, lodash’s main package (lodash) is written in CommonJS module format, so ES Modules-based Tree Shaking does not work properly. Therefore, even when using Named Imports, the bundler has no choice but to include the entire library.
Solution
1. Changing Import Paths
The solution is surprisingly simple. Just change the import paths to directly import from lodash’s individual function modules.
- import { cloneDeep } from 'lodash'
+ import cloneDeep from 'lodash/cloneDeep'
- import { isEqual } from 'lodash'
+ import isEqual from 'lodash/isEqual'
When importing from individual paths like lodash/cloneDeep, the bundler only includes the code for that specific function. There’s no need to load the entire library.
2. Scale of Changes
This was by no means a one or two file change. We had to modify a total of 1,012 files across the entire monorepo.
| App/Package | Files Modified |
|---|---|
| App A | 394 |
| App B | 300 |
| App C | 182 |
| App D | 114 |
| App E | 9 |
| Code Generation Tool | 5 |
| Shared Packages | 8 |
3. Preventing Recurrence with ESLint Rules
Fixing the problem is only half the battle — it’s equally important to prevent the same issue from occurring again. We added ESLint’s no-restricted-imports rule to the shared configuration so that an error is thrown when code directly imports from lodash.
// Shared ESLint configuration
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'lodash',
message:
"For Tree Shaking, please use import fn from 'lodash/fn' instead of import { fn } from 'lodash'.",
},
],
},
],
Thanks to this rule, developers receive an immediate warning at the lint stage if they accidentally write code in the import { cloneDeep } from 'lodash' format.
For more details on ESLint import-related rules, check out eslint-plugin-import. Also, ESLint configuration for monorepo environments is covered in VSCode ESLint Configuration for Monorepo.
4. Updating Code Generation Tools
We also updated the templates of the project’s code generation tools (scaffolding). By ensuring the correct import method is applied from the start when generating new pages or features, we achieved consistency across the entire development workflow.
Results
Bundle Size Changes
The size lodash occupies in the bundle decreased dramatically.
547KB → 97KB (approximately 82% reduction)
The same level of improvement was confirmed across all apps in the monorepo.
Expected Benefits
- Faster initial loading: A reduction of approximately 450KB of JavaScript leads to shorter parsing and execution times.
- Reduced network costs: A meaningful reduction in transfer size is expected even after gzip compression.
- Improved mobile user experience: Particularly effective in constrained network environments.
Lessons Learned
1. Small Changes, Big Impact
It’s remarkable that changing a single import path can reduce bundle size by 82%. This is a prime example of “low-hanging fruit” in frontend optimization.
2. Bundle Analysis is Essential
To recognize a problem, you must first measure it. Regular bundle analysis using tools like rollup-plugin-visualizer is the starting point for optimization.
3. Enforce with Rules
Code reviews alone cannot catch every case. By adding automated verification through ESLint rules, the entire team can maintain the correct patterns without extra effort.
4. Update Code Generation Tools Too
If your project uses scaffolding tools, the optimized patterns should be reflected in the templates as well. Otherwise, the same problem will recur every time new code is generated.
5. Consider Alternatives
While we solved this by changing import paths, other approaches exist.
- lodash-es: An ES Modules version of lodash that enables Tree Shaking even with Named Imports.
- babel-plugin-lodash / eslint-plugin-lodash: Plugins that automatically transform imports during the build.
- Native alternatives: Browser built-in APIs such as
structuredClone()(replaces cloneDeep) andObject.is()(partially replaces isEqual).
Conclusion
Frontend performance optimization often starts not with grand architectural changes, but with careful code management like this case. The cycle of discovering problems through bundle analysis, fixing them systematically, and preventing recurrence with lint rules is the key to maintaining a healthy frontend codebase.
If your project also uses lodash, I recommend trying a bundle analysis. You might be able to reduce hundreds of KBs right now.
There are various other ways to improve frontend performance. Consider also looking into image format optimization, web font loading optimization, and React rendering performance optimization.
You can also measure performance using Lighthouse. For details, refer to the Comprehensive Lighthouse Performance Optimization Guide.
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku.Deku created the applications with Flutter.If you have interested, please try to download them for free.