We frequently encounter a situation: a React-based e‑commerce site takes 8 seconds to load on mobile devices. The reason is a monolithic JS bundle weighing 2 MB. The browser must download, parse, and execute all code before displaying any content. This worsens INP and FCP, reducing conversion by 30%. Our team offers a systematic bundle optimization: route-based code splitting, tree shaking, lazy imports, and replacing heavy libraries. Turnkey — analysis, rework, CI monitoring. We guarantee to reduce initial JS to 50 kB. We can assess your project in 2 days.
Bundle Analysis and Problem Discovery
# Vite — visualization via rollup-plugin-visualizer
npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'vendor-query': ['@tanstack/react-query'],
},
}
},
chunkSizeWarningLimit: 500,
}
});
After the build, an interactive bundle map opens. We look for large libraries (moment.js, lodash), duplicate dependencies, and whole-library imports instead of the needed function. On average such an audit takes 1–2 hours and reveals the main "fat" spots.
Route-Based Code Splitting
Code splitting divides the bundle into parts per route. The user downloads only the code for the current page. This reduces initial load size and speeds up INP. For React we use React.lazy and Suspense:
// React Router v6 — lazy loading pages
import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const ProductCatalog = lazy(() => import('./pages/ProductCatalog'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));
const router = createBrowserRouter([
{ path: '/catalog', element: <Suspense fallback={<PageSkeleton />}><ProductCatalog /></Suspense> },
{ path: '/products/:slug', element: <Suspense fallback={<PageSkeleton />}><ProductDetail /></Suspense> },
{ path: '/cart', element: <Suspense fallback={<PageSkeleton />}><Cart /></Suspense> },
{ path: '/checkout', element: <Suspense fallback={<PageSkeleton />}><Checkout /></Suspense> },
]);
// Dynamic import of heavy components (editor, charts, maps)
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const Chart = lazy(() => import('./components/Chart'));
function ProductForm() {
const [showEditor, setShowEditor] = useState(false);
return (
<>
<button onClick={() => setShowEditor(true)}>Add description</button>
{showEditor && (
<Suspense fallback={<div>Loading editor...</div>}>
<RichTextEditor />
</Suspense>
)}
</>
);
}
Tree Shaking and Library Replacement
Tree shaking removes unused code, reducing JS size. This decreases parsing and execution time, improving FCP and INP. Vite and Webpack automatically exclude dead code when imports are organized correctly.
// Bad: import entire lodash (~70kB gzip)
import _ from 'lodash';
const sorted = _.sortBy(products, 'price');
// Good: import only the needed function
import sortBy from 'lodash/sortBy';
const sorted = sortBy(products, 'price');
// Even better: native JS
const sorted = [...products].sort((a, b) => a.price - b.price);
// date-fns instead of moment.js
import { format, addDays } from 'date-fns'; // tree-shakeable
import { ru } from 'date-fns/locale';
Library replacement table
| Library | Replacement | Savings |
|---|---|---|
| moment.js (72kB) | date-fns (only needed functions) | ~60kB (3.6× smaller) |
| lodash (70kB) | lodash-es + tree-shaking | ~50kB |
| axios (13kB) | native fetch | 13kB |
| jquery (87kB) | Native JS | 87kB |
| react-icons (all) | Only needed from @heroicons | 100–500kB |
Why Tree Shaking Doesn’t Always Work?
Tree shaking is effective only for ES modules. If a library exports CommonJS (require), dead code won’t be removed. Check if the package supports "type": "module" or use lodash-es instead of lodash. Also ensure your project does not enable the @rollup/plugin-commonjs plugin without the transformMixedEsModules option. In SPAs this issue is common, especially with older packages. We help migrate to modern alternatives, which improves Core Web Vitals.
What’s Included in the Work
- Analysis of the current build and a report with recommendations.
- Implementation of code splitting and lazy loading.
- Replacement of heavy libraries while preserving functionality.
- Tree shaking setup and chunk optimization.
- Integration of bundle size monitoring into CI/CD.
- Documentation and guide for future optimization.
- Engineer support for 2 weeks after deployment.
Optimization Process
- Audit the current bundle using Vite visualizer or Webpack Bundle Analyzer.
- Identify large libraries and duplicates.
- Develop a code splitting strategy by routes and components.
- Replace heavy libraries with lightweight alternatives.
- Configure tree shaking and manual chunks in Vite/Rollup.
- Implement prefetching of critical chunks (prefetch on hover).
- Integrate bundle size checks into CI.
- Document results and train the team.
Timelines and Results
From 2 to 5 days depending on project complexity. Includes audit, code splitting, library replacement, and CI setup. We guarantee to reduce initial JavaScript to 50 kB. This optimization saves traffic and improves user experience, directly impacting conversion and revenue. Order an audit to get specific numbers for your project.
Case Study
For one e‑commerce site (React, Next.js), the original bundle weighed 1.5 MB. After page-level code splitting and replacing moment.js with date-fns, initial JS dropped to 180 kB. LCP improved from 4.2 to 1.1 seconds, and INP from 450 to 120 ms. Conversion increased by 12%. The entire project took 4 days. CDN traffic savings were approximately 30% per month.
CI Monitoring and Metrics
Target values for an e‑commerce site (gzip):
| Chunk | Target |
|---|---|
| Initial JS (critical path) | < 50 kB |
| React + React DOM | ~42 kB |
| Catalog page | < 30 kB |
| Product page | < 20 kB |
| Cart/Checkout | < 40 kB |
# .github/workflows/bundle-size.yml
- name: Check bundle size
run: |
npm run build
MAIN_JS=$(ls dist/assets/index-*.js | xargs stat -c%s | head -1)
if [ "$MAIN_JS" -gt 200000 ]; then
echo "Bundle too large: ${MAIN_JS} bytes"
exit 1
fi
Learn more about JavaScript modules in the MDN documentation. We guarantee improvement of Core Web Vitals metrics. Contact us to get a consultation on your project. Accelerating website loading is an investment in its success, and we are here to help achieve it.







