Do You Still Need useMemo in React 19? Here's When It Matters

React 19 changed everything about performance optimization. The new React Compiler handles most memoization automatically, which means the optimization patterns you learned in React 18 are now mostly unnecessary.

But "mostly" isn't "always." This guide explains what's changed, what still matters, and practical patterns for building fast React apps that score well on Core Web Vitals.

What changed in React 19

The React Compiler

React 19's biggest performance feature isn't a hook - it's the compiler. During build time, it analyzes your components and automatically:

  • Skips unnecessary re-renders
  • Memoizes expensive calculations
  • Stabilizes function references
  • Optimizes prop comparisons

This happens without you writing any optimization code. The compiler figures out what needs memoization and adds it for you.

// React 18: You wrote this
const ExpensiveComponent = memo(({ data }) => {
  const processed = useMemo(() => heavyCalculation(data), [data]);
  const handleClick = useCallback(() => doSomething(data), [data]);

  return <div onClick={handleClick}>{processed}</div>;
});

// React 19: Just write this
function ExpensiveComponent({ data }) {
  const processed = heavyCalculation(data);
  const handleClick = () => doSomething(data);

  return <div onClick={handleClick}>{processed}</div>;
}
// The compiler adds memoization where needed

What this means for you

Write simpler code. Don't add useMemo, useCallback, or memo by default. Let the compiler optimize.

Profile before optimizing. If you see a performance issue, then investigate. Don't preemptively add hooks "just in case."

The hooks still exist. They're not deprecated. There are cases where manual control is needed.

When you still need manual optimization

The compiler is smart, but it's not magic. Here's when manual optimization still matters:

1. Third-party library constraints

Some libraries expect stable references. If a library re-initializes when a callback changes, you need useCallback:

function MapComponent({ markers }) {
  // MapLibrary re-initializes if onMarkerClick changes
  // Compiler might not catch this library-specific behavior
  const onMarkerClick = useCallback((marker) => {
    console.log('Clicked:', marker.id);
  }, []);

  return <MapLibrary markers={markers} onMarkerClick={onMarkerClick} />;
}

2. Effects with function dependencies

When a function is in an effect's dependency array, you might need useCallback to prevent infinite loops:

function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  // Without useCallback, this creates a new function every render
  // causing the effect to run infinitely
  const fetchData = useCallback(async () => {
    const result = await api.getUser(userId);
    setData(result);
  }, [userId]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return <div>{data?.name}</div>;
}

Better pattern: Move the function inside the effect when possible:

function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const result = await api.getUser(userId);
      setData(result);
    }
    fetchData();
  }, [userId]);

  return <div>{data?.name}</div>;
}

3. Expensive calculations with external data

If a calculation depends on external data that changes frequently, you might want explicit memoization:

function SearchResults({ query, allItems }) {
  // allItems has 10,000 entries and changes rarely
  // query changes on every keystroke
  // Compiler might not optimize this perfectly
  const filteredItems = useMemo(() => {
    return allItems.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [query, allItems]);

  return <List items={filteredItems} />;
}

4. Context value stability

Context values that are objects need memoization to prevent all consumers re-rendering:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  // Without useMemo, this object is recreated every render
  // causing all useContext(ThemeContext) consumers to re-render
  const value = useMemo(() => ({
    theme,
    setTheme,
    isDark: theme === 'dark',
  }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

Patterns that actually improve performance

Lazy loading components

Don't load everything upfront. Split your bundle and load components when needed:

import { lazy, Suspense } from 'react';

// Loaded only when rendered
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function Dashboard({ user }) {
  return (
    <div>
      <Header user={user} />

      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart data={user.analytics} />
      </Suspense>

      {user.isAdmin && (
        <Suspense fallback={<Loading />}>
          <AdminPanel />
        </Suspense>
      )}
    </div>
  );
}

This directly improves LCP by reducing initial JavaScript. If you're using Material UI, lazy loading heavy components like DataGrid and DatePicker is essential - see our MUI performance guide for specifics.

Virtualize long lists

Rendering thousands of items kills performance. Use virtualization:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualItem.start}px)`,
              height: `${virtualItem.size}px`,
            }}
          >
            {items[virtualItem.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Optimize images properly

Image optimization has a bigger impact than most React patterns. Use proper loading strategies:

function ProductImage({ src, alt, isLCP }) {
  return (
    <img
      src={src}
      alt={alt}
      // LCP images should load eagerly
      loading={isLCP ? 'eager' : 'lazy'}
      // Prevent CLS
      width={400}
      height={300}
      // Hint browser about importance
      fetchPriority={isLCP ? 'high' : 'auto'}
    />
  );
}

For Next.js users, use next/image which handles this automatically:

import Image from 'next/image';

function HeroImage({ src }) {
  return (
    <Image
      src={src}
      alt="Hero"
      width={1200}
      height={600}
      priority // Marks as LCP image
    />
  );
}

Debounce expensive operations

For search inputs or other frequent updates, debounce to reduce work:

import { useDeferredValue } from 'react';

function SearchableList({ items }) {
  const [query, setQuery] = useState('');

  // React 19: useDeferredValue handles this elegantly
  const deferredQuery = useDeferredValue(query);

  // This expensive filter uses the deferred value
  // UI stays responsive while filtering happens in background
  const filteredItems = items.filter(item =>
    item.name.toLowerCase().includes(deferredQuery.toLowerCase())
  );

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {/* Show stale indicator while updating */}
      <div style={{ opacity: query !== deferredQuery ? 0.7 : 1 }}>
        <List items={filteredItems} />
      </div>
    </div>
  );
}

Use transitions for non-urgent updates

Mark state updates as non-urgent so they don't block user input:

import { useTransition } from 'react';

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function handleTabChange(newTab) {
    startTransition(() => {
      setTab(newTab); // This update won't block input
    });
  }

  return (
    <div>
      <TabButtons
        onSelect={handleTabChange}
        disabled={isPending}
      />
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        <TabContent tab={tab} />
      </div>
    </div>
  );
}

This improves INP by keeping interactions responsive.

Performance anti-patterns to avoid

Don't: Premature optimization

// Bad: Adding hooks "just in case"
function SimpleButton({ onClick, children }) {
  const memoizedOnClick = useCallback(onClick, [onClick]);
  return <button onClick={memoizedOnClick}>{children}</button>;
}

// Good: Keep it simple
function SimpleButton({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

The compiler handles this. Don't add complexity for no measured benefit.

Don't: State in wrong location

// Bad: Parent re-renders everything on input change
function Parent() {
  const [search, setSearch] = useState('');

  return (
    <div>
      <SearchInput value={search} onChange={setSearch} />
      <ExpensiveList /> {/* Re-renders on every keystroke */}
      <AnotherComponent />
    </div>
  );
}

// Good: Co-locate state with component that uses it
function Parent() {
  return (
    <div>
      <SearchWithResults /> {/* State lives here */}
      <ExpensiveList />
      <AnotherComponent />
    </div>
  );
}

function SearchWithResults() {
  const [search, setSearch] = useState('');
  // Only this component re-renders on input
  return (
    <>
      <SearchInput value={search} onChange={setSearch} />
      <Results query={search} />
    </>
  );
}

Don't: Inline object/array creation in JSX

// Bad: New object every render
<Component style={{ marginTop: 20 }} />
<List items={[1, 2, 3]} />

// Good: Define outside component or use useMemo if dynamic
const style = { marginTop: 20 };
const items = [1, 2, 3];

<Component style={style} />
<List items={items} />

This matters when passing to memoized children. It's especially relevant for MUI's sx prop - using sx in loops can add significant overhead. See MUI performance optimization for when to use styled() instead.

Measuring React performance

Don't optimize blindly. Use these tools:

React DevTools Profiler

  1. Install React DevTools browser extension
  2. Open DevTools → Profiler tab
  3. Click record, interact with your app, stop recording
  4. Look for components that render often or take long

Chrome DevTools Performance

For Core Web Vitals specifically:

  1. Open DevTools → Performance tab
  2. Record a page load
  3. Look at the "Timings" section for LCP, CLS markers
  4. Check "Main" thread for long tasks affecting INP

Web Vitals library

Add real user monitoring:

import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

Checklist: React + Core Web Vitals

For LCP

  • Lazy load below-fold components with React.lazy
  • Use priority on Next.js Image for hero images
  • Minimize JavaScript bundle size
  • Server-side render critical content

For CLS

  • Set explicit dimensions on images
  • Reserve space for dynamic content
  • Avoid inserting content above existing content
  • Use CSS transform for animations, not layout properties

For INP

  • Use useTransition for non-urgent updates
  • Use useDeferredValue for expensive computations
  • Virtualize long lists
  • Keep event handlers fast, move heavy work to effects

Frequently Asked Questions

Is useMemo obsolete in React 19?

Not obsolete, but rarely needed. The React Compiler handles most memoization automatically. Only add useMemo manually when you've measured a specific performance issue that the compiler doesn't catch.

Should I remove all my existing useMemo and useCallback hooks?

Not immediately. They still work and won't hurt performance. But for new code, start without them and only add if needed. Gradually simplify existing code when you're confident the compiler handles it.

How do I know if the React Compiler is working?

Check your build output for the compiler's transformations, or use React DevTools which shows when components skip re-renders. If you're on React 19 with a modern build setup, it's likely active.

What's the biggest performance win in React?

Usually it's not React-specific: optimize images, reduce JavaScript bundle size, and server-render critical content. These affect Core Web Vitals more than any hook optimization.

Does React 19 help with INP?

Yes. The automatic memoization reduces unnecessary re-renders, and features like useTransition and useDeferredValue help keep interactions responsive. But you still need to use them correctly.

What's next

React performance starts with measurement. Before optimizing anything, run your site through PageSpeedFix to identify what's actually hurting your Core Web Vitals.

Often the fix isn't a React pattern at all - it's an unoptimized image, render-blocking script, or missing server-side rendering. Fix the big issues first, then fine-tune React-specific patterns if needed.

Framework and library guides: