Bundle Size, Rendering & React Performance
Code-splitting, lazy loading, render minimisation, and the React-specific patterns that come up in audits.
Code-Splitting & Lazy Loading
// Dynamic import — splits into a separate chunk
const HeavyChart = lazy(() => import('./HeavyChart'))
<Suspense fallback={<Spinner />}>
<HeavyChart data={data} />
</Suspense>
// Route-level splitting (built into Next.js, Remix, TanStack Router).
// Don't ship the admin bundle to anonymous visitors.
// Prefetch on intent — start downloading the chunk when the user hovers
<Link to="/dashboard" onMouseEnter={() => import('./pages/Dashboard')}>
Dashboard
</Link>
// Conditionally import heavy libraries — only when needed
async function exportPdf(data) {
const {default: jsPDF} = await import('jspdf') // 200KB, not in main bundle
new jsPDF().text('Report', 10, 10).save()
}Bundle Analysis
# Next.js — built-in analyzer
ANALYZE=true pnpm build
# Opens treemap of every chunk. Look for:
# - Duplicate libraries (lodash + lodash-es, multiple date libs)
# - Heavy dependencies in client chunks (zod schema in client, etc.)
# - Polyfills bloating modern browsers
# Vite / Rollup
pnpm add -D rollup-plugin-visualizer
# Adds stats.html after build
# Webpack
pnpm add -D webpack-bundle-analyzer
# CLI tools
npx source-map-explorer dist/**/*.js
npx bundlephobia <package-name> # before adding a dep
# Common wins:
# - date-fns → import {format} from 'date-fns/format' (tree-shakeable)
# - lodash → lodash-es with named imports, or write the helper yourself
# - moment.js → swap for date-fns or dayjs (~100KB saved)
# - icon libraries → import individual icons, not the whole packReact Render Optimisation
// React 19 + the React Compiler auto-memoize — much of the below is then
// redundant. Audit your project's compiler status first.
// 1) Memoize expensive computations
const sorted = useMemo(
() => items.toSorted((a, b) => b.score - a.score),
[items]
)
// 2) Memoize callbacks passed to memoized children
const handleClick = useCallback((id) => onSelect(id), [onSelect])
// 3) Wrap pure components with React.memo
const Row = memo(function Row({user}) { return <li>{user.name}</li> })
// 4) Keys must be stable and unique — never the array index for dynamic lists
{items.map(it => <Row key={it.id} user={it} />)}
// 5) Split state — local state shouldn't trigger global re-renders
// Don't put input value in the top-level store; keep it in the component.
// 6) Lift state DOWN. The component that owns state re-renders.
// Move state into the leaf if no sibling needs it.
// 7) Use the React DevTools Profiler.
// 'Why did this render?' tells you the changed prop.Image Optimisation
// Next.js — handles format, sizes, lazy loading, blur placeholder
import Image from 'next/image'
<Image
src="/hero.jpg"
alt=""
width={1200}
height={600}
priority // = preload + fetchpriority high. Above-the-fold only.
placeholder="blur" // tiny base64 blurred preview
sizes="(max-width: 768px) 100vw, 1200px"
/>
// Vanilla HTML — responsive srcset
<picture>
<source type="image/avif" srcset="hero.avif 1x, hero@2x.avif 2x">
<source type="image/webp" srcset="hero.webp 1x, hero@2x.webp 2x">
<img src="hero.jpg" alt="" width="1200" height="600"
loading="lazy" decoding="async">
</picture>
// Targets to remember
// AVIF / WebP — 30-50% smaller than JPEG for the same quality
// Hero images — ≤ 200KB on mobile, ≤ 400KB on desktop
// Lazy-load — everything below the fold (loading="lazy")Caching & Network
Long-lived immutable cache for hashed assets: Cache-Control: public, max-age=31536000, immutable
Short cache + stale-while-revalidate for HTML/JSON: max-age=0, s-maxage=60, stale-while-revalidate=600
Prefetch only on intent (hover/focus). Predictive prefetch on every link wastes data.
HTTP/3 + Brotli on the CDN edge. Most platforms (Vercel, Cloudflare) ship this by default — verify in DevTools.
Service Worker can absorb repeat traffic. See the PWA notes for cache strategies.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free