Bundle Size: The Diet for your JavaScript
JavaScript is the most expensive resource on the web. A technical guide to Tree Shaking, Code Splitting, and using Partytown to survive the Third-Party Script apocalypse.
“The site is fast on my MacBook Pro.” This is the most dangerous sentence in frontend engineering. Your MacBook Pro has an M3 Max chip. It parses 1MB of JavaScript in 50ms. Your user has a $200 Samsung Galaxy A15. It parses that same 1MB in 2.5 seconds. During those 2.5 seconds, the main thread is frozen. The user clicks “Add to Cart”. Nothing happens. They leave.
In 2025, Network Speed (4G/5G) is rarely the bottleneck for e-commerce. CPU is the bottleneck. And the metric that tracks this is Interaction to Next Paint (INP). To fix INP, you must reduce the JavaScript Payload.
At Maison Code Paris, we enforce strict budgets: <100KB Initial Load.
Why Maison Code Discusses This
At Maison Code Paris, we act as the architectural conscience for our clients. We often inherit “modern” stacks that were built without a foundational understanding of scale. We see simple APIs that take 4 seconds to respond because of N+1 query problems, and “Microservices” that cost $5,000/month in idle cloud fees.
We discuss this topic because it represents a critical pivot point in engineering maturity. Implementing this correctly differentiates a fragile MVP from a resilient, enterprise-grade platform that can handle Black Friday traffic without breaking a sweat.
1. The Cost of Imports: Tree Shaking
Tree Shaking (Dead Code Elimination) is the process of removing unused exports from your bundle. But it doesn’t work by magic. You must write code that is “Shakeable”.
The Barrel File Trap
Developers love “Barrel Files” (index.ts that exports everything).
They destroy Tree Shaking.
Bad Pattern:
// components/index.ts
export * from './Button';
export * from './Carousel'; // Giant 50kb component
export * from './Table';
// App.tsx
import { Button } from './components';
In many bundler configurations (especially older Webpack), importing Button will also include Carousel in the bundle, because of side-effect detection limitations.
Fix: Direct Imports or careful sideEffects: false configuration in package.json.
The Library Trap (Lodash)
import { map } from 'lodash'; pulls in the entire 70KB library.
Use lodash-es or specific imports: import map from 'lodash/map';.
Even better, use native array methods. [].map() costs 0 bytes.
2. Code Splitting: Lazy Loading Routes
Why send the “Admin Dashboard” code to a customer who is just buying a shirt? We use Route-Based Code Splitting.
In Next.js (App Router), this happens automatically for page.tsx files.
In Vite/React Router, you must use lazy().
import { lazy, Suspense } from 'react';
// This dynamic import creates a separate chunk (e.g., Settings.chunk.js)
const SettingsPage = lazy(() => import('./pages/Settings'));
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/settings" element={
<Suspense fallback={<Spinner />}>
<SettingsPage />
</Suspense>
} />
</Routes>
);
}
Component-Level Splitting
Is your “3D Model Viewer” heavy? Don’t load it on page load. Load it on interaction.
const ModelViewer = dynamic(() => import('./ModelViewer'), { ssr: false });
function ProductPage() {
const [show3D, setShow3D] = useState(false);
return (
<>
<button onClick={() => setShow3D(true)}>View in 3D</button>
{show3D && <ModelViewer />}
</>
);
}
The heavy JS is only downloaded when the user actually asks for it.
3. Visualizing the Bloat
You cannot fix what you cannot see.
We use @next/bundle-analyzer (or rollup-plugin-visualizer).
Run it before every deployment.
You will find horrors:
- Three.js inside the homepage bundle?
- Moment.js locales? (Use
date-fnsorIntlAPI). - Faker.js in production? (It should be a devDependency).
We typically reduce bundle size by 30% just by deleting unused dependencies found in the visualizer.
4. Third-Party Scripts: The “Partytown” Solution
You optimized your application code to 50KB. Perfect. Then the Marketing Team adds GTM (Google Tag Manager). GTM injects:
- Facebook Pixel (40KB)
- TikTok Pixel (50KB)
- Hotjar (70KB)
- Klaviyo (40KB)
- Gorgias (200KB)
Suddenly, your main thread is blocked by 500KB of marketing scripts. The solution is Off-Main-Thread Architecture.
We use Partytown.
It runs third-party scripts in a Web Worker.
It proxies DOM access (e.g., document.cookie) from the Worker to the Main Thread via synchronous XHR (Atomic Wait).
// layout.tsx (Next.js)
import { Validation } from './Partytown';
<Head>
<Partytown debug={true} forward={['dataLayer.push']} />
</Head>
<Script
src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX"
type="text/partytown" // Magic attribute
/>
Result: The Facebook Pixel runs on a background thread. It calculates tracking data without freezing your “Add to Cart” button. INP improves dramatically.
5. Modern Formats: Shipping Less Code
Polyfills are Obsolete
Are you supporting Internet Explorer 11? No.
Stop shipping polyfills for Promise, Map, Set, fetch.
Set your browserslist target to defaults, not IE 11.
This removes ~30KB of legacy junk.
Brotli Compression
Gzip is good. Brotli is better. Brotli (br) compresses JavaScript ~20% better than Gzip. Ensure your CDN (CloudFront/Vercel) has Brotli enabled.
6. Image Optimization: The Hidden Payload
JavaScript is the CPU bottleneck, but Images are the Bandwidth bottleneck. If you load a 2MB PNG on a 4G connection, the JS download is delayed. Strategy:
- Format: Use AVIF. It is 30% smaller than WebP.
- Lazy Loading:
<img loading="lazy">for everything below the fold. - Dimensions: Always set
widthandheightto prevent Layout Shifts (CLS). - CDN: Use Cloudinary or Imgix to resize on the fly.
image.jpg?w=400&q=auto
7. Font Loading Strategy
Fonts are render-blocking. If your OTF files are 100KB, the text is invisible (FOIT) for 2 seconds. Fix:
- Self-Host: Do not use Google Fonts. The DNS lookup catches you.
- Subset: Remove glyphs you don’t use (e.g., Cyrillic characters).
- Display Swap:
font-display: swap;. Show the fallback font immediately. - Preload:
<link rel="preload" href="/font.woff2" as="font">for the Heading font only.
8. The “Import Cost” DX
Developers are lazy. If they can import a library, they will. Install the Import Cost extension in VS Code. It displays the size of the import inline as you type.
import { format } from 'date-fns'; // 14KB (Gzipped)
This instant feedback loop makes developers think twice before adding a dependency.
8. The Future: Resumability (Qwik)
React’s fundamental flaw is Hydration.
Hydration means: “Download the HTML. Then download the JS. Then run the JS to attach event listeners.”
It is duplicated work.
Frameworks like Qwik introduce Resumability.
There is no Hydration.
The event listener is serialized into the HTML: <button on:click="./chunk.js#handleClick">.
The JS for the click handler is only downloaded when the user actually clicks.
If they never click, you download 0KB of JS.
This is the “O(1) JavaScript” ideal. We are watching this closely for 2026.
9. React Server Components (RSC)
Next.js App Router uses RSC.
RSC allows you to keep dependencies on the server.
Before RSC:
import { format } from 'date-fns' -> Client bundle increases by 20KB.
After RSC:
import { format } from 'date-fns' -> Runs on server. Renders HTML. Client bundle increases by 0KB.
Strategy: Move all heavy formatting, data fetching, and markdown processing to Server Components.
Keep Client Components only for interactivity (useState, useEffect).
“Leaf Nodes” should be Client Components. “Container Nodes” should be Server Components.
9. The 100kb Budget Rule
We have a strict rule: You cannot merge a PR that pushes the Initial Bundle over 100kb.
We enforce this via CI.
bundlesize check fails:
“Error: Main Bundle is 105kb. Limit is 100kb.” This forces a conversation. “Do we really need this library? Can we lazy load it?” If you don’t enforce it, the bundle will grow infinitely. Every KB must fight for its life.
10. Conclusion
Performance is not a “nice to have”. It is revenue. Amazon found that 100ms of latency cost them 1% of sales. Bloated JavaScript IS latency. Treat your Bundle Size budget like a financial budget. If you exceed it, you must “pay it back” by deleting code.
Is your site heavy?
Does your Lighthouse Performance score show “Reduce Unused JavaScript”?
Performance is not a “nice to have”. It is revenue. Amazon found that 100ms of latency cost them 1% of sales. Bloated JavaScript IS latency. Treat your Bundle Size budget like a financial budget. If you exceed it, you must “pay it back” by deleting code.
Is your site heavy?
Does your Lighthouse Performance score show “Reduce Unused JavaScript”?