Skip to main content

React Performance: When One Slow Component Freezes Everything

React Performance: When One Slow Component Freezes Everything

So, this came up in an interview recently: there was a single slow component I was not allowed to change. I fixed it with a quick React.memo wrap during the interview, but later I thought, hey, why stop there? Let me share how I handled it and some other ways to tackle this problem.

TL;DR

Got a slow component that's freezing your app? Can't touch its code? Here's your survival kit:

  • React.memo - Stop pointless re-renders when props haven't changed
  • State isolation - Move the slow stuff away from your fast UI
  • Lazy loading - Make it someone else's problem (later)
  • useDeferredValue - Let users type while the slow component catches up
  • Nuclear options - iframe, web worker, or virtual scrolling when all else fails

Try the solutions yourself in the sandbox below

Open in CodeSandbox

The Problem

There’s a slow component I can’t change. My job is to keep the UI responsive while it exists on the page.

Goals:

  • Keep typing fast.
  • Avoid re-rendering the slow component on every keystroke.
  • Improve initial load if possible.
function App() {
  const [value, setValue] = React.useState("");
  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <VerySlowComponent />
      <span>Length: {value.length}</span>
    </div>
  );
}

We cannot edit <VerySlowComponent />. Everything else is fair game.

Solutions

1. React.memo (quick win)

Wrap your slow component in React.memo to tell React, “Hey, only re-render me if my props actually change.” This works great as long as you don’t pass any changing props. It’s like putting a “Do Not Disturb” sign on your component’s door.

const SlowComponent = React.memo(function SlowComponent({ data }) {
  const result = heavyCalculation(data);
  return <div>{result}</div>;
});

Now, SlowComponent chills and only wakes up when data changes, not every time unrelated state updates happen. Nice!

2. Isolate State

Move stateful input into a separate subtree so the slow component never re-renders. Keep the input and its state in a different branch; render the slow part as a sibling, memoized. No props flow from the input to the slow component, so it stays idle.

// Isolate state in a sibling subtree
import { memo, useState } from "react";

const MemoSlow = memo(VerySlowComponent);

function FastPanel() {
  const [value, setValue] = useState("");
  return (
    <>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <div>Length: {value.length}</div>
    </>
  );
}

function App() {
  return (
    <div style={{ display: "grid", gap: 12 }}>
      <MemoSlow />
      <FastPanel />
    </div>
  );
}

Because the FastPanel’s state changes don’t flow into MemoSlow, the slow component never re-renders.

3. Lazy Load the Slow Component

Why load the slow component right away if you don’t need it? Use React.lazy and Suspense to load it only when you want it (saving your app from feeling sluggish at startup).

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

function App() {
  const [show, setShow] = React.useState(false);

  return (
    <>
      <button onClick={() => setShow(!show)}>Toggle Slow Component</button>
      <React.Suspense fallback={<div>Loading...</div>}>
        {show && <SlowComponent />}
      </React.Suspense>
    </>
  );
}

This way, your app loads faster and only asks the slow component to join the party when needed. This does not stop re-renders later; it improves first paint.

4. Defer Non-Urgent Updates

Another option is to defer updates so typing stays smooth. In this example, we still render the slow component but lazy load it, and pass in the input value. Typing stays responsive while the slow part shows up with a loading fallback.

import { Suspense, lazy, useState } from "react";

const Slow = lazy(() =>
  import("./shared/VerySlowComponent").then((m) => ({
    default: m.VerySlowComponent,
  }))
);

function App() {
  const [value, setValue] = useState("");

  return (
    <div style={{ display: "grid", gap: 8 }}>
      <input
        placeholder="Type something"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <Suspense fallback={<p>Loading slow part…</p>}>
        <Slow query={value} />
      </Suspense>
      <div>Length: {value.length}</div>
    </div>
  );
}

This way you keep the input responsive, get a nice loading state, and still handle the slow component without blocking typing.

5. Defer with useDeferredValue and useTransition

React 18 gives us concurrent features to mark updates as non-urgent. With useDeferredValue and useTransition, you can keep typing responsive while slow components update a bit later.

import { useState, useDeferredValue, useTransition } from "react";
import { VerySlowComponent } from "./shared/VerySlowComponent";

export default function DeferTransition() {
  const [value, setValue] = useState("");
  const deferred = useDeferredValue(value);
  const [, startTransition] = useTransition();

  return (
    <div>
      <input
        value={value}
        onChange={(e) => {
          const v = e.target.value;
          startTransition(() => setValue(v)); // mark as non-urgent
        }}
      />
      <div>Length (deferred): {deferred.length}</div>
      <VerySlowComponent />
    </div>
  );
}

This lets React prioritize the input first, then update the slow part in the background, keeping the UI feeling snappy.

6. Stronger Isolation Techniques

If your slow component is still being a party pooper, think about isolating it completely:

  • Put it in an iframe or a web worker (because sometimes you need to put it in timeout).
  • Use virtualization libraries like react-window or react-virtualized to handle huge lists efficiently.
  • Break the component into smaller, less scary pieces.

Try order

  1. memo if props are stable
  2. Isolate state into a sibling subtree
  3. Lazy load for faster first paint
  4. Defer with useDeferredValue/useTransition
  5. Stronger isolation (portals, separate root)

Conclusion

When one slow component tries to hog the spotlight and freeze your whole app, don’t panic! With some smart state management, memoization, lazy loading, and React’s fancy concurrent features, you can keep your app smooth and your users happy. Your React app deserves to be fast — no potato-powered freezes allowed!

More posts

  • Claude Code: Part 11 - Troubleshooting and Recovery

    August 9, 2025

    Master Claude Code troubleshooting with systematic approaches to common issues: installation problems, CLAUDE.md conflicts, performance optimization, custom commands, MCP servers, hooks, and emergency recovery procedures.

  • Claude Code: Part 10 - Common Issues and Quick Fixes

    August 8, 2025

    Solve the most common Claude Code problems: context overflow, conflicting rules, token optimization, and broken custom commands. Quick troubleshooting for experienced users.

  • Claude Code: Part 9 - Complete Development Workflows

    August 7, 2025

    Learn how to combine all Claude Code features into complete development workflows. From feature planning to deployment, see how CLAUDE.md, slash commands, MCP servers, subagents, IDE integration, and hooks work together for seamless development.