The Complete Frontend Optimization Guide
Stop guessing why your React app is slow. A step-by-step audit guide covering Vite chunking, Nginx Gzip compression, and strategic lazy loading.
Frontend Performance Guidelines
Last Updated: November 2025
Scope: React, Vite, Nginx, Infrastructure
Phase 1: The Audit (Start Here)
Before writing code, you must identify what is slowing us down.
Step 1: Build for Production
Never audit using npm run dev. The development server uses uncompressed, unbundled files.
npm run build
npm run preview
# Opens the app on http://localhost:4173 (usually)
Step 2: The Network Tab Check
Open Chrome DevTools (F12) -> Network tab.
- Disable Cache: Check the box.
- Filter: Click “JS”.
- Refresh the page.
🚩 Red Flags to look for:
- Massive Files: Is there a single file larger than 300kB? (This suggests we aren’t splitting code correctly).
- Waterfall Queue: Are files downloading one by one instead of all at once? (This suggests HTTP/2 is missing).
- Size vs. Transferred: Is the “Transferred” size significantly smaller (approx 30%) than “Resource” size? If they are the same, Gzip is off.
Step 3: The Bundle Anatomy
Run the visualizer to see exactly what libraries are inside our bundle.
# Ensure rollup-plugin-visualizer is in vite.config.ts
npm run build
# Open the generated stats.html file in your browser
🚩 Red Flags:
- Seeing
lodashas one massive block (instead of small scattered pieces). - Seeing heavy libraries (
recharts,jspdf) inside theindex.jsorvendor.json the Login page.
Phase 2: Code Optimization (React & Vite)
Strategy: “Load Less, Load Later.”
2.1 Route-Based Splitting (The Baseline)
Rule: Every top-level page in app.tsx must be lazy-loaded.
❌ Bad (Eager): Forces code into the initial bundle.
import Dashboard from './pages/Dashboard';
✅ Good (Lazy): Isolates code into a separate chunk.
import { lazy } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
2.2 Component-Level Splitting (The “Dashboard Rule”)
Context: If a page loads immediately after login (like a Dashboard), lazy-loading the route isn’t enough. If that Dashboard imports a heavy Chart library, that library is still downloaded on login.
Rule: Isolate heavy UI elements within the page.
❌ Bad:
// Dashboard.tsx
import { BigChart } from 'recharts'; // ⚠️ Pulls 500kB into the main bundle!
export const Dashboard = () => (
<div>
<h1>Stats</h1>
<BigChart data={data} />
</div>
);
✅ Good:
// Dashboard.tsx
import { lazy, Suspense } from 'react';
// 1. Dynamic Import: Vite detects this and creates a separate "vendor-charts.js" chunk
const BigChart = lazy(() => import('./components/BigChart'));
export const Dashboard = () => (
<div>
<h1>Stats</h1>
{/* 2. Suspense: The text loads instantly; the chart loads in parallel */}
<Suspense fallback={<div className="h-64 bg-gray-100 animate-pulse" />}>
<BigChart data={data} />
</Suspense>
</div>
);
2.3 Event-Driven Loading
Rule: If a library is only used when a user clicks a button (e.g., PDF generation), load it inside the event handler.
const handleExport = async () => {
// ⏳ User clicks -> Browser downloads library -> Library executes
const { default: jsPDF } = await import('jspdf');
const doc = new jsPDF();
doc.save('report.pdf');
};
2.4 Vite Configuration
Rule: Trust the defaults. Do not use custom manualChunks strategies that force node_modules into a single file.
✅ Correct vite.config.ts:
build: {
rollupOptions: {
output: {
// Let Vite verify dependencies automatically.
// Only add manualChunks if you need to group specific related libs (e.g. all graphing libs).
}
}
}
Phase 3: Server Optimization (Nginx)
Great code is slow on a bad server. Verify these settings.
3.1 Enable HTTP/2 (Multiplexing)
Why: Allows the browser to download hundreds of small JS chunks in parallel.
Check: In Nginx config (/etc/nginx/sites-available/default), ensure http2 is in the listen directive.
server {
listen 443 ssl http2; # <--- IMPORTANT
# ... SSL cert paths ...
}
3.2 Enable Gzip Compression
Why: Reduces text file size by ~70%.
Check: Add this block to your http or server block.
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Balance between CPU usage and compression size
gzip_types
text/plain
text/css
application/json
application/javascript
text/xml
application/xml
application/xml+rss
text/javascript;
Phase 4: Assets & Media
4.1 Fonts
Rule: Self-host fonts. Never use fonts.googleapis.com.
- Download
.woff2files. - Place in
public/fonts/. - Preload critical weights (Regular/Bold) in
index.html.
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin />
4.2 Images
Rule: Use .webp format.
- Resize images to 2x their display size (Retina support).
- Use
fetchpriority="high"on the largest image visible above the fold (LCP).
Phase 5: The Code Review Checklist
Before merging a PR, verify:
- New Pages: Are they wrapped in
lazy()? - New Heavy Libraries: Are they loaded dynamically (either via component lazy-load or
await import)? - Imports: Are we importing specifically? (e.g.,
import { format } from 'date-fns'instead of the whole library). - Build Check: Did you run
npm run buildand check the file sizes? Are there any sudden spikes in the main vendor chunk?
Let's Build Something Scalable
We apply these same engineering principles to client projects. Ready to upgrade your stack?