Performance Optimization
02 / 02

Bundle Size, Rendering & React Performance

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 pack

React 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