React

React Performance Optimization Techniques

Practical strategies to speed up your React apps and improve Core Web Vitals.

September 15, 2025
8 min read
By useLines Team
ReactPerformanceCore Web VitalsOptimization
Illustration for React Performance Optimization Techniques

React performance, in short

Focus on fewer renders, fewer DOM nodes, fewer bytes, and deferring non‑critical work.

Measure first

  • Use Performance panel and React Profiler
  • Track Core Web Vitals (LCP, INP, CLS)

Cut re‑renders

  • Stabilize props with useMemo/useCallback only where profiling shows churn
  • Split large context or use selector hooks
  • Prefer derived UI over duplicating state

Render less

  • Virtualize long lists (react‑window)
  • Code‑split heavy routes/widgets (React.lazy + Suspense)

Ship fewer bytes

  • Optimize images (sizes/srcset, modern formats, lazy)
  • Cache API responses; debounce inputs; compress over the wire

Keep interactions snappy

  • useTransition/useDeferredValue for expensive updates during typing
  • Avoid layout thrash; prefer transform/opacity for animation

Checklist: stable props, virtualize big lists, optimize images, code‑split, verify with Web Vitals.

const ExpensiveList = memo(({ items }) => {
  const sorted = useMemo(() => items.slice().sort(), [items]);
  const handleClick = useCallback((id) => console.log(id), []);
  return sorted.map((id) => (
    <button key={id} onClick={() => handleClick(id)}>
      {id}
    </button>
  ));
});

Virtualize long lists

Use windowing libraries for lists > 100 items.

Code-splitting

Lazy-load non-critical routes and components with React.lazy and Suspense.

Image optimization

Serve responsive images, use modern formats (WebP/AVIF), and lazy-load offscreen images.

Measure first

Profile with the React DevTools Profiler and browser performance panel before changing code.

Profile React the right way

Start with the Performance tab and React Profiler:

  1. Record interactions that feel slow (route change, list filter, opening modals).
  2. Identify components with long render times and high commit counts.
  3. Check for “cascading re-renders” where a parent re-renders many children.

If the flamegraph shows many renders without state changes, you likely have unstable props or context redraws.

Prevent unnecessary re-renders

  • Pass stable references: move inline objects/functions out or wrap with useMemo/useCallback when they cause real re-render cost.
  • Split context: large contexts force wide re-renders; split by concern or use selector-based contexts.
  • Prefer derived UI over derived state: compute UI from raw state rather than storing multiple copies.
// Bad: new object every render → child re-renders
<Chart options={{ color: theme.primary }} />;

// Good: stable memoized value
const chartOptions = useMemo(() => ({ color: theme.primary }), [theme.primary]);
<Chart options={chartOptions} />;

React.memo and when to use it

React.memo can cut re-renders of pure components. It helps most when:

  • Props rarely change
  • Component work is expensive (formatting, heavy DOM, large lists)

Beware of overusing it—memoization has a comparison cost. Add it where profiling shows benefit.

const Card = React.memo(function Card({ title, children }) {
  return (
    <section className="blog-card rounded-xl p-6">
      <h3 className="font-bold mb-2">{title}</h3>
      {children}
    </section>
  );
});

Virtualize really long lists

For 100+ items, render only what’s visible. Libraries like react-window or react-virtualized reduce DOM nodes dramatically.

npm i react-window
import { FixedSizeList as List } from "react-window";

const Row = ({ index, style, data }) => (
  <div style={style}>{data[index].title}</div>
);

export function PostsVirtualized({ items }) {
  return (
    <List
      height={480}
      itemCount={items.length}
      itemSize={56}
      width={"100%"}
      itemData={items}
    >
      {Row}
    </List>
  );
}

Optimize images aggressively

  • Always include intrinsic width and height or container ratios to reduce CLS.
  • Use loading="lazy" and decoding="async" (already in this project’s LazyImage).
  • Serve multiple sizes with srcset/sizes when possible.
<img
  src="/img/hero-1280.jpg"
  srcset="
    /img/hero-640.jpg   640w,
    /img/hero-960.jpg   960w,
    /img/hero-1280.jpg 1280w
  "
  sizes="(max-width: 768px) 100vw, 768px"
  alt="Hero"
  loading="lazy"
  decoding="async"
/>

Code-split non-critical UI

Lazy-load heavy widgets and route-level bundles:

const Comments = React.lazy(() => import("./Comments"));

export function PostFooter() {
  return (
    <React.Suspense fallback={<div>Loading comments…</div>}>
      <Comments />
    </React.Suspense>
  );
}

Network and runtime tweaks

  • Cache API responses; debounce input-driven queries.
  • Avoid large JSON in memory; stream where possible.
  • Use a CDN for static assets; prefer Brotli compression.

Tie improvements to Web Vitals

Track LCP, INP, and CLS. This project already includes a perf utility (src/utils/performance.js). Ensure analytics hooks are configured so you can verify impact.

Schedule work to keep interactions snappy

Use concurrent features to prevent UI jank during keystrokes and navigation.

import { useDeferredValue, useTransition } from "react";

export function Search({ list }) {
  const [term, setTerm] = React.useState("");
  const [isPending, startTransition] = useTransition();

  // Defer filtering to keep typing responsive
  const deferredTerm = useDeferredValue(term);
  const results = React.useMemo(
    () => list.filter((x) => x.includes(deferredTerm)),
    [list, deferredTerm]
  );

  function onChange(e) {
    const value = e.target.value;
    startTransition(() => setTerm(value));
  }

  return (
    <div>
      <input value={term} onChange={onChange} />
      {isPending && <span>Updating…</span>}
      <ul>
        {results.map((r) => (
          <li key={r}>{r}</li>
        ))}
      </ul>
    </div>
  );
}

Avoid layout thrashing

Batch reads and writes and minimize sync layout measurements in hot paths.

// Bad: forces layout on every mousemove
element.addEventListener("mousemove", () => {
  const { left } = element.getBoundingClientRect(); // read
  element.style.transform = `translateX(${left}px)`; // write
});

// Better: requestAnimationFrame and cached reads
let frame = 0;
element.addEventListener("mousemove", () => {
  if (frame) return;
  frame = requestAnimationFrame(() => {
    const left = cachedLeft; // from a scheduled read
    element.style.transform = `translateX(${left}px)`;
    frame = 0;
  });
});

Use CSS where possible (e.g., transform, opacity) and hint with will-change for highly animated elements.

Data fetching and Suspense boundaries

Wrap slow subtrees in Suspense with lightweight fallbacks to keep the page interactive.

const Pricing = React.lazy(() => import("./Pricing"));

export function Page() {
  return (
    <React.Suspense fallback={<div className="h-32 animate-pulse" />}>
      <Pricing />
    </React.Suspense>
  );
}

Hydration pitfalls

  • Avoid reading window/document during initial render; guard with effects or environment checks.
  • Keep server and client markup consistent (no randomized IDs without stable seeds).
  • Provide explicit width/height to media to avoid layout shifts post-hydration.

Checklist

  • Stable props and memoization for hot paths
  • Virtualized lists for large collections
  • Optimized images with proper sizing and lazy loading
  • Strategic code-splitting for heavy components
  • Monitored Web Vitals before/after changes

Conclusion

Measure first, optimize where it hurts, and verify with profiling tools. Most wins come from reducing work (renders, DOM, bytes) and deferring what the user doesn’t need immediately.

Related Posts