Client Bundle Optimization — Minimizing JavaScript with RSC
In this tutorial, you will learn about Client Bundle Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.
React Server Components reduce the client JavaScript bundle by keeping server-only code and dependencies on the server, sending only interactive component code to the browser.
What You'll Learn
You will understand how RSC affect bundle size, techniques for measuring and optimizing client bundles, and patterns for keeping client JavaScript minimal.
Why It Matters
Smaller bundles mean faster page loads, lower data usage, and better Core Web Vitals. RSC provide a structural advantage for bundle optimization.
Real-World Use
Durga Antivirus Pro reduced its main client bundle from 320KB to 195KB by moving threat analysis, reporting, and data transformation components from Client to Server Components.
flowchart LR
A[All Components] --> B{Component Type}
B -->|Server Component| C[Stays on Server]
B -->|Client Component| D[Bundled for Client]
C --> E[No JS sent]
D --> F[Only this JS ships]
B --> G{Has dependencies}
G -->|Server-only lib| H[Stays on server]
G -->|Client-safe lib| I[Included in bundle]
style C fill:#1e293b,color:#fff
style D fill:#0f172a,color:#fff
style F fill:#0f172a,color:#fff
style H fill:#1e293b,color:#fff
How RSC Reduce Bundle Size
When a Server Component imports a heavy library, that library stays on the server and never reaches the client.
// Server Component — this library stays on the server
import { parseMarkdown } from 'heavy-markdown-library';
import { format } from 'date-fns';
async function BlogPost({ postId }) {
const post = await db.posts.findById(postId);
const html = parseMarkdown(post.content); // Server only
return (
<article>
<h1>{post.title}</h1>
<time>{format(post.date, 'MMMM d, yyyy')}</time>
<div dangerouslySetInnerHTML={{ __html: html }} />
</article>
);
}
Expected output: The heavy-markdown-library and date-fns are imported only on the server. The client receives no JavaScript for these imports. The rendered HTML contains the parsed markdown result.
Measuring Bundle Impact
Use Next.js built-in bundle analyzer to measure what ships to the client.
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
Expected output: Running ANALYZE=true next build generates an interactive treemap of your client bundle. Server Component code does not appear in the client bundle.
Moving Logic to Server Components
Identify computations and data transformations that can move from Client to Server Components.
// BEFORE — Client Component does heavy computation
'use client';
function ThreatReport({ rawData }) {
const processedData = rawData.map(t => ({
...t,
severity: t.score > 8 ? 'CRITICAL' : t.score > 5 ? 'HIGH' : 'LOW',
riskLevel: calculateRisk(t),
formattedDate: format(new Date(t.timestamp), 'PPpp'),
}));
return <DataTable data={processedData} />;
}
// AFTER — Server Component processes, Client only renders
async function ThreatReport({ reportId }) {
const rawData = await db.threats.findById(reportId);
const processedData = rawData.map(t => ({
...t,
severity: t.score > 8 ? 'CRITICAL' : t.score > 5 ? 'HIGH' : 'LOW',
riskLevel: calculateRisk(t),
formattedDate: format(new Date(t.timestamp), 'PPpp'),
}));
return <DataTable data={processedData} />;
}
Expected output: The heavy computation (calculateRisk, date formatting) moves to the server. The DataTable Client Component receives pre-processed data and only handles rendering and interactivity.
Dynamic Imports for Client Components
Use dynamic imports to split large Client Components into separate chunks.
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Skip SSR for this component
});
const CodeEditor = dynamic(() => import('./CodeEditor'), {
loading: () => <p>Loading editor...</p>,
});
export default function AnalysisPage() {
return (
<div>
<h1>Analysis</h1>
<HeavyChart />
<CodeEditor />
</div>
);
}
Expected output: HeavyChart and CodeEditor load as separate JavaScript chunks. They are only downloaded when the page renders them. The SSR: false option prevents server-rendering for components that need browser APIs.
Tree Shaking with Server and Client Separation
Keep server-only utilities in separate files from client code to help tree-shaking.
// lib/server-utils.js — only imported by Server Components
import { encrypt, decrypt } from 'crypto';
import { readFileSync } from 'fs';
export function processSensitiveData(data) { /* ... */ }
// lib/client-utils.js — only imported by Client Components
export function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
}
Expected outcome: Server utilities are never included in the client bundle. Client utilities are tree-shaken to include only what is actually used in Client Components.
Common Mistakes
Importing heavy visualization libraries in Server Components unnecessarily: If the chart needs client-side interactivity, keep it in a Client Component. But the data preparation for the chart can be a Server Component.
Not using dynamic imports for large Client Components: Dynamic imports defer loading until the component is needed, reducing the initial bundle size.
Putting all logic in Client Components out of habit: Default to Server Components. Move code to Client Components only when interactivity is required.
Including polyfills in Server Components: Polyfills modify browser globals. They are only needed in Client Components. Server Components do not need them.
Not auditing the bundle regularly: Bundle size creeps up over time. Use bundle analysis tools in CI to catch regressions.
Practice Questions
- How do Server Components reduce bundle size?
They keep server-only dependencies and logic on the server. Only the rendered HTML reaches the client, not the JavaScript for those components.
- What tool can visualize your Next.js client bundle?
@next/bundle-analyzer generates an interactive treemap showing the size of each module in the client bundle.
- How do dynamic imports help bundle optimization?
They split large components into separate chunks that load on demand instead of in the initial bundle.
- What is the default for components in Next.js App Router?
All components are Server Components by default. You opt into client rendering with
'use client'.
- Why should you keep server utilities in separate files?
To prevent accidental import of server-only modules into client code, which would either cause errors or unnecessarily increase the bundle.
Challenge
Analyze an existing Next.js app's bundle using the bundle analyzer, identify the three largest Client Components, and refactor at least one to move data processing to a Server Component.
Frequently Asked Questions
Mini Project
Take an existing Client Component that renders a data table with sorting, filtering, and export. Move the data fetching and processing to a Server Component. The Client Component should receive pre-processed data and only handle the interactive features (sort, filter, export).
What's Next
Learn about Next.js App Router RSC to understand how RSC integrate with Next.js routing.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro