Home / Articles / React performance beyond hand-placed memo: structure and scheduling

This article is published in English.

React performance beyond hand-placed memo: structure and scheduling

Compiler-era advice: colocate state, separate urgent work, ship less JS, then memoize measured hotspots.

803 words

For years, React performance advice defaulted to wrapping everything in memo APIs:

const value = useMemo(() => expensiveCalculation(data), [data]);
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);export default React.memo(Component);
function ProductList({ products, onSelect }) {
  const processed = useMemo(
    () => processProducts(products),
    [products]
  );
  const handleSelect = useCallback(
    (id) => onSelect(id),
    [onSelect]
  );  return (
    <List
      products={processed}
      onSelect={handleSelect}
    />
  );
}
App
 │
 ├── Header
 ├── Sidebar
 ├── Search
 ├── ProductList
 └── Footer
function App() {
  const [query, setQuery] = useState("");
  // large component tree
}
App
 │
 ├── Header
 ├── Sidebar
 ├── Search
 │    └── query state
 ├── ProductList
 └── Footer
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
function handleChange(event) {
  const value = event.target.value;  setQuery(value);  startTransition(() => {
    setSearchResults(filterProducts(value));
  });
}
User input
    │
    ├── urgent ───────► keep UI responsive
    │
    └── non-urgent ───► transition
const AnalyticsDashboard = lazy(
  () => import("./AnalyticsDashboard")
);
Initial JavaScript
       │
       ▼
┌─────────────────────┐
│ Everything          │
│ Dashboard           │
│ Analytics           │
│ Editor              │
│ Admin               │
└─────────────────────┘
Initial load
    │
    ├── Core UI
    │
    └── Later
          ├── Analytics
          ├── Editor
          └── Admin
"This component renders often."
        │
        ▼
Add useMemo
"This component renders often."
        │
        ▼
Why?
        │
 ┌──────┼───────────┐
 ▼      ▼           ▼
State  Expensive   Large
flow   work        subtree
 │      │           │
 ▼      ▼           ▼
Colocate  Optimize  Restructure
1. Measure
      ↓
2. Find the expensive work
      ↓
3. Fix component architecture
      ↓
4. Reduce unnecessary JavaScript
      ↓
5. Prioritize updates correctly
      ↓
6. Let React Compiler handle memoization
      ↓
7. Manually optimize only when evidence says you should

Memoization alone does not fix slow apps. A perfectly memoized tree can still thrash from oversized state, urgent updates doing non-urgent work, or heavy JavaScript on the main thread.

React Compiler changes the default

The compiler automates many memo slots. That shifts human effort toward structure and scheduling—not hand-placed useMemo everywhere.

First: move state closer to where it’s used

High state in the tree re-renders broad subtrees. Colocate state with the leaves that need it; lift only when sharing requires it.

Second: don’t make urgent updates do non-urgent work

Keep typing and animation responsive. Defer non-urgent work with transitions/deferred values so keystrokes are not blocked by expensive filters.

Third: look at the JavaScript you’re shipping

Large sync loops, expensive formatters, and unbounded lists dominate before React’s reconciler does. Profile the JS, not only React DevTools highlights.

So when should you use useMemo?

Still useful for referential stability across children that bail out on props, and for truly expensive pure calculations—after measuring. Not as default clothing for every value.

The new React performance mindset

Structure → scheduling → shipping less JS → then micro-memoize hotspots. Compiler-assisted memo is a helper, not a substitute for design.

Add a performance budget to PRs: interaction to next paint for the main form, and a React profiler snapshot for the known heavy route.

Lists should virtualize before they memoize every row. Memo on a 10k-row mount still loses.

Add a performance budget to PRs: interaction to next paint for the main form, and a React profiler snapshot for the known heavy route.

Lists should virtualize before they memoize every row. Memo on a 10k-row mount still loses.

Add a performance budget to PRs: interaction to next paint for the main form, and a React profiler snapshot for the known heavy route.

Lists should virtualize before they memoize every row. Memo on a 10k-row mount still loses.

Add a performance budget to PRs: interaction to next paint for the main form, and a React profiler snapshot for the known heavy route.

Lists should virtualize before they memoize every row. Memo on a 10k-row mount still loses.

Add a performance budget to PRs: interaction to next paint for the main form, and a React profiler snapshot for the known heavy route.

Lists should virtualize before they memoize every row. Memo on a 10k-row mount still loses.

Add a performance budget to PRs: interaction to next paint for the main form, and a React profiler snapshot for the known heavy route.

Lists should virtualize before they memoize every row. Memo on a 10k-row mount still loses.