The Frontend Performance Optimization Checklist You'll Actually Use
A practical, prioritized guide to making your React app fast
Performance advice on the internet falls into two categories: obvious tips you already know ("minimize your JavaScript") and exotic micro-optimizations that make zero difference for 99% of apps ("use WebAssembly for your todo list"). Here's something different: a prioritized checklist based on what actually moves the needle, ordered by impact.
I've optimized dashboards from 11-second renders to sub-second. I've cut bundle sizes by 60%. And the pattern is always the same — 3-4 high-impact changes make 90% of the difference.
Priority 1: Don't Ship What Users Don't Need
The fastest code is code that never runs. Before you optimize how your JavaScript executes, optimize how much of it you send.
Code Splitting by Route
If your entire app is a single JavaScript bundle, a user visiting your landing page downloads the code for your dashboard, settings page, and every other route. Fix this first.
// Next.js: Automatic. Each page is its own bundle.
// React + Vite: Dynamic imports
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Analyze Your Bundle
You can't optimize what you can't see.
# Next.js
ANALYZE=true npm run build
# Vite
npx vite-bundle-visualizer
Look for:
- Large dependencies you don't need. I once found a 200KB date library used for a single
formatDatecall. Replaced with 2 lines of Intl.DateTimeFormat. - Duplicate packages. Multiple versions of the same library bundled separately.
- Unused exports. Libraries that don't tree-shake properly.
The Heavy Library Audit
Check your top 5 heaviest dependencies. For each one, ask: "Can I replace this with a smaller alternative or native API?"
| Heavy | Lightweight Alternative | |-------|------------------------| | moment.js (300KB) | date-fns (tree-shakeable) or Intl API (0KB) | | lodash (70KB) | lodash-es (tree-shakeable) or native methods | | chart.js (200KB) | Recharts (tree-shakeable) | | axios (13KB) | Native fetch (0KB) |
Priority 2: Optimize Images
Images are typically 50-70% of a page's total weight. This is the highest-ROI optimization for most websites.
Use Next.js Image Component
import Image from "next/image";
// Automatic: WebP/AVIF conversion, responsive sizes, lazy loading
<Image
src="/hero.jpg"
alt="Dashboard screenshot"
width={1200}
height={630}
priority // Only for above-the-fold images
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
For Non-Next.js Apps
<!-- Responsive images with modern formats -->
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img
src="hero.jpg"
alt="Dashboard screenshot"
loading="lazy"
decoding="async"
width="1200"
height="630"
/>
</picture>
The Image Checklist
- [ ] All images have explicit
widthandheight(prevents layout shift) - [ ] Below-the-fold images use
loading="lazy" - [ ] Above-the-fold images use
priorityorfetchpriority="high" - [ ] Images are served in WebP or AVIF format
- [ ] Images are sized appropriately (don't serve 4000px images on mobile)
Priority 3: Fix Rendering Performance
Identify Unnecessary Re-renders
Install React DevTools Profiler. Record an interaction. Look for components that re-render when they shouldn't.
Common culprits:
// BAD: New object created every render, child always re-renders
<ChildComponent style={{ color: "red" }} />
// GOOD: Stable reference
const style = useMemo(() => ({ color: "red" }), []);
<ChildComponent style={style} />
// BAD: New function created every render
<Button onClick={() => handleClick(item.id)} />
// GOOD: Stable callback
const handleItemClick = useCallback((id: string) => {
handleClick(id);
}, [handleClick]);
Virtualize Long Lists
Any list with 50+ items should be virtualized. Period.
import { useVirtualizer } from "@tanstack/react-virtual";
// Instead of rendering 1,000 items:
// Render only ~20 visible items + 10 buffer items
Debounce Expensive Operations
// Search input: don't fetch on every keystroke
const debouncedSearch = useDebouncedCallback((query: string) => {
fetchResults(query);
}, 300);
<input onChange={(e) => debouncedSearch(e.target.value)} />
Priority 4: Core Web Vitals
Google measures three metrics. Optimize them in this order:
LCP (Largest Contentful Paint) — Target: < 2.5s
The time until the largest visible element renders. Usually a hero image or heading.
- Preload the LCP image:
<link rel="preload" as="image" href="hero.webp"> - Use
priorityon Next.js Image for hero images - Eliminate render-blocking CSS (inline critical CSS)
- Use a CDN for static assets
CLS (Cumulative Layout Shift) — Target: < 0.1
Visual stability. Things shouldn't jump around as the page loads.
- Always set
widthandheighton images and videos - Reserve space for dynamic content (skeleton loaders)
- Don't inject content above existing content
- Use
font-display: swapfor web fonts
INP (Interaction to Next Paint) — Target: < 200ms
How quickly the page responds to user input.
- Keep the main thread free (no synchronous heavy computation)
- Use
startTransitionfor non-urgent state updates - Break up long tasks with
requestIdleCallback - Virtualize long lists
The 80/20 Summary
If you only do four things:
- Code split by route — don't ship code users don't need
- Optimize images — biggest weight savings for least effort
- Virtualize long lists — from 10,000 DOM nodes to 200
- Memoize expensive computations — stop recalculating what hasn't changed
These four changes will make more difference than every micro-optimization combined. Start here. Measure. Then decide if you need to go deeper.