Performance and Best Practices

Introduction

React 18 is fast by default, but large lists, unnecessary re-renders, and bundle bloat still slow real apps. This chapter covers practical habits—lazy routes, memo, useMemo, project structure, and accessibility—without premature optimization.

Prerequisites

Measure Before Optimizing

  1. React DevTools — Profiler tab, highlight updates
  2. Browser Performance — long tasks, layout thrashing
  3. Network — bundle size, API waterfall
  4. Lighthouse — LCP, INP, CLS

Fix the biggest bottleneck first—often network or one heavy component.

Compare Vue performance chapter.

Route and Component Lazy Loading

tsx
const ReportsPage = lazy(() => import("./pages/ReportsPage"));
 
<Suspense fallback={<p>Loading…</p>}>
  <ReportsPage />
</Suspense>

Manual vendor chunks in vite.config.ts:

typescript
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ["react", "react-dom", "react-router-dom"],
      },
    },
  },
},

React.memo

Skip re-render when props are unchanged:

tsx
import { memo } from "react";
 
type RowProps = { id: string; label: string };
 
export const HeavyRow = memo(function HeavyRow({ id, label }: RowProps) {
  return <tr><td>{id}</td><td>{label}</td></tr>;
});

Use when Profiler shows expensive child re-rendering with same props—not everywhere by default.

useMemo and useCallback

tsx
const filtered = useMemo(
  () => todos.filter((t) => !t.done),
  [todos]
);
 
const handleSave = useCallback(() => {
  saveTodo(draft);
}, [draft]);
HookWhen it helps
useMemoExpensive filter/sort; stable object for effect deps
useCallbackPass stable fn to memo child

Avoid wrapping every function—measure first.

List Performance

ProblemDirection
10,000 DOM rowsVirtual scroll (@tanstack/react-virtual)
Unstable keyStable ids, not index
Filter in renderuseMemo filtered list

Do not render 10k <tr> elements—virtualize.

Zustand Selectors

tsx
// Re-renders on any store change
const store = useTodoStore();
 
// Better — subscribe to slice
const items = useTodoStore((s) => s.items);

Image and Asset Loading

tsx
<img src="/hero.webp" alt="Hero" loading="lazy" width={800} height={400} />
  • Modern formats (WebP, AVIF)
  • Explicit dimensions reduce CLS
  • loading="lazy" below the fold

Keep Components Focused

SmellFix
500-line componentSplit UI + custom hooks
God componentFeature folders + Zustand
Duplicate fetchusePosts() hook
text
src/features/posts/
├── components/
├── hooks/
├── stores/
├── pages/
└── types.ts

Accessibility Basics

  • Semantic HTML: <button> not <div onClick>
  • <label htmlFor="email"> linked to inputs
  • Visible focus styles
  • aria-live for dynamic errors

Accessible CSS, HTML accessible forms.

Security Habits

  • No dangerouslySetInnerHTML on user content
  • CSP headers in production — Nginx

Anti-Patterns

Anti-patternWhy
useEffect for everythingDerive in render or event handlers
Prop drilling 10 levelsContext or Zustand
Mutate state in placeBreaks React updates
memo every componentWasted comparison cost
Giant bundle importLazy routes + tree-shake icons

FAQ

React slower than Vue?

Both are fast; architecture and bundle size matter more.

Context performance?

Split contexts; prefer Zustand for frequent updates.

Devtools "rendered because parent rendered"?

Lift memo, stabilize callbacks, or split state.

SSR for performance?

SSR helps first paint—not always faster TTI; measure.

Import entire icon library?

Import individual icons—full library bloats bundle.

Profile hook cost?

Hooks are cheap—optimize render work and child trees.