EmpireUI
Get Pro
← Blog9 min read#react performance#profiler#compiler

React Performance in 2026: Profiler, Compiler, Memo — What Still Matters

React 19's compiler changes what you actually need to optimise by hand. Here's what still matters — Profiler, Memo, useMemo, useCallback — and what you can finally stop worrying about.

Code editor screen showing React JavaScript performance optimization code

The State of React Performance in 2026

React 19 shipped the React Compiler (formerly "React Forget") as stable in late 2024, and by 2026 the majority of new projects are using it. That changes the performance conversation quite a bit. A lot of the optimisations you used to do manually — wrapping everything in useMemo, sprinkling useCallback all over your custom hooks, React.memo-ing every leaf component — the compiler handles automatically now. Honestly, some of the advice that dominated dev Twitter in 2022 is actively wrong in 2026.

That said, the compiler doesn't solve every problem. It can't fix bad data-fetching patterns. It can't fix a component tree that re-renders 800 nodes because one atom changed in your global store. And it absolutely can't tell you *why* your app feels slow — for that, you still need the React Profiler. The tool has barely changed since React 16.5 introduced it, but it remains the most honest diagnostic you have.

This article is for developers who already know the basics and want a clear-eyed picture of what actually matters in 2026 — what the compiler handles for you, what it doesn't, and where manual intervention still pays off. We're also going to look at how component-heavy UIs (like the ones you'd build with Empire UI) interact with the React rendering model, because that's a real-world scenario that exposes performance constraints quickly.

What the React Compiler Actually Handles (and What It Doesn't)

The React Compiler transforms your component code during the build step and inserts memoisation automatically. If you have a component that derives some value from props and renders JSX, the compiler will wrap that derivation in the equivalent of useMemo and skip re-renders when inputs haven't changed. It targets the same patterns you'd previously optimise by hand — and in most cases does a better job, because it analyses dependencies precisely rather than relying on you to list them correctly.

// Before compiler: you wrote this manually
const filteredItems = useMemo(
  () => items.filter(i => i.active),
  [items]
);

// With compiler: just write the obvious thing
const filteredItems = items.filter(i => i.active);
// Compiler inserts the memoisation for you

What the compiler *doesn't* handle: side effects, async boundaries, imperative DOM manipulation, and any code that runs outside the React transform pipeline (think third-party scripts, non-standard build setups, or code paths the compiler deems "unsafe" to auto-memo). In practice, the compiler opts out of memoising code that writes to refs, calls non-pure functions, or has patterns it can't statically analyse — and it tells you so in the build output.

Worth noting: the compiler doesn't fix architectural problems. A flat component tree that passes a 2,000-item array down six levels of props will still re-render all six levels when the array changes, even with the compiler running. The optimisations it makes are *local* — they don't rethink data flow. That's still your job.

One more thing — you can check whether the compiler processed a specific component by enabling babel-plugin-react-compiler with logger: true. It emits a per-file summary of what was memoised and what was skipped, which is genuinely useful when you're debugging unexpected re-renders.

Using the React Profiler Correctly in 2026

The Profiler tab in React DevTools (v5.x) hasn't changed dramatically since 2021, but most developers use it wrong. They record a session, see a bunch of yellow bars, panic, and start wrapping things in memo. The useful workflow is different. Start a recording, interact with the *specific* slow path, stop recording, then use the "Why did this render?" tooltip on the slowest component. That tells you exactly what prop or state change triggered it.

// Programmatic profiling when DevTools isn't available
import { Profiler } from 'react';

function onRenderCallback(
  id: string,
  phase: 'mount' | 'update' | 'nested-update',
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number
) {
  if (actualDuration > 16) {
    // Anything > 16ms breaks 60fps
    console.warn(`[Profiler] ${id} took ${actualDuration.toFixed(1)}ms (${phase})`);
  }
}

<Profiler id="ProductList" onRender={onRenderCallback}>
  <ProductList items={items} />
</Profiler>

The baseDuration value is the one people overlook. It represents how long rendering would take *without* any memoisation — a theoretical worst case. If your actualDuration is close to baseDuration, memoisation isn't saving you anything, which usually means dependency arrays are stale or the component is receiving new object references on every render. If actualDuration is near zero but the frame rate is still bad, the bottleneck is likely outside the React tree — layout thrashing, JS thread contention, or a heavy CSS animation on the compositor thread.

In 2026, 16ms per frame (60fps) is still the baseline target for most apps. For anything shown on a high-refresh display (120Hz is default on most phones and many laptops since 2024), you're targeting 8ms. That sounds brutal, but React's concurrent renderer helps by breaking work across frames — as long as you're not blocking the main thread in your render functions.

React.memo, useMemo, useCallback — What's Still Worth Writing by Hand

Look, the compiler handles a lot of memoisation automatically now, but there are cases where you still reach for these tools manually. The main one is stable function references passed to child components that opt into their own update logic — specifically, components connected to external event emitters, canvas renderers, or virtualized lists where every new function reference forces a full re-initialisation.

// Still worth writing by hand: stable callback for an event emitter child
const handleSelect = useCallback(
  (id: string) => {
    setSelected(id);
    analytics.track('item_selected', { id });
  },
  [setSelected] // analytics is stable, so not listed
);

// Also manual: heavy computation across a large dataset
const stats = useMemo(
  () => computeAggregates(rawData), // expensive O(n²) operation
  [rawData]
);

React.memo is still worth writing manually on components that receive complex objects as props and don't benefit from compiler-level memoisation — typically because they're defined in a pattern the compiler can't fully analyse, or they're imported from a library that hasn't been compiled. The rule I use: if the component takes more than 2ms to render (per Profiler) and its props are unlikely to change on every parent update, add React.memo explicitly. Below 2ms, don't bother.

In practice, the best performance optimisation you can do in 2026 is pick the right state management pattern upfront. Global stores that broadcast every update to every subscriber, context values that wrap large sub-trees, or prop drilling six levels deep — those cause more re-renders than the compiler can fix. Zustand's fine-grained subscriptions, Jotai's atomic model, or even TanStack Query's server-state separation solve most performance problems before you ever need to open the Profiler.

Component Libraries and Render Performance

If you're building something visually ambitious — say, a SaaS dashboard with glassmorphism cards, animated transitions, and a live data feed — the component library you choose matters more than people admit. Libraries that render deeply nested wrapper elements, apply inline styles on every update, or rely on re-computing CSS-in-JS on every render will burn your render budget fast. That's a real reason to prefer utility-class-based libraries like Empire UI that emit stable class names rather than computing styles at runtime.

Quick aside: the glassmorphism components in Empire UI are a good example of how static Tailwind classes keep render overhead negligible. There's no CSS-in-JS runtime, no style object recomputation — the classes are fixed strings. Even with the compiler off, a glassmorphism card re-renders in under 0.5ms because there's nothing expensive happening in the render function.

Where component libraries do hurt performance is in animation. If you're running 30+ simultaneously animated elements and each one has a JS-driven animation (requestAnimationFrame in user code, not a CSS transition), you'll saturate the JS thread quickly. The fix is always the same: push animations to CSS transforms and opacity where possible, since those run on the compositor thread and bypass the JS budget entirely.

// Bad: JS-driven position animation burns JS thread
useEffect(() => {
  const raf = requestAnimationFrame(() => {
    ref.current.style.left = `${pos}px`; // triggers layout
  });
  return () => cancelAnimationFrame(raf);
}, [pos]);

// Better: CSS transform — compositor thread, zero JS cost
<div style={{ transform: `translateX(${pos}px)` }} />
// Or even better: a CSS transition with a class toggle

If you're building a UI that mixes heavy visuals with live data — think trading dashboards, real-time collaboration tools, or AI-driven interfaces — test on a mid-range Android device from 2024, not your M3 MacBook. Performance problems that don't show up on your dev machine are almost always visible on hardware with a lower memory bandwidth and a slower GPU.

Code Splitting and Lazy Loading in 2026

Code splitting hasn't changed conceptually since React 16.6 introduced React.lazy, but the tooling around it got significantly better. In 2026, Next.js 15's dynamicIO mode and Vite 6's module graph analysis handle most route-level splitting automatically. What you still need to do manually: split heavy UI panels, rich-text editors, chart libraries, and anything that imports a large third-party dependency.

import { lazy, Suspense } from 'react';

// The chart library (recharts, visx, etc.) is ~200KB gzipped
// Don't load it until the analytics panel is actually opened
const AnalyticsPanel = lazy(() => import('./AnalyticsPanel'));

function Dashboard() {
  const [showAnalytics, setShowAnalytics] = useState(false);
  return (
    <>
      <button onClick={() => setShowAnalytics(true)}>View Analytics</button>
      {showAnalytics && (
        <Suspense fallback={<div className="animate-pulse h-48 bg-white/10 rounded-2xl" />}>
          <AnalyticsPanel />
        </Suspense>
      )}
    </>
  );
}

That Suspense fallback matters more than people think. A 48px blank flash is jarring. A skeleton that roughly matches the shape of the incoming content (same height, same border radius) reduces perceived load time significantly — it's a 2016 idea that still works in 2026. If you're using Empire UI's glassmorphism components, a bg-white/10 backdrop-blur-sm rounded-2xl animate-pulse skeleton matches the aesthetic perfectly with three utility classes.

One underused pattern: prefetch on hover. If you know a user is likely to click something — they've hovered for 100ms — start importing the lazy component before they click. Next.js does this automatically for <Link> components in the viewport, but for panel-based UIs you need to wire it yourself with a onMouseEnter handler that calls import('./HeavyComponent') and throws the promise away. The browser caches the module; the actual render just has to wait for the module to be evaluated.

The Practical Optimisation Checklist for 2026

Stop chasing micro-optimisations before you've measured. The React Profiler, Chrome's Performance tab, and web-vitals together give you an honest view of where time is actually spent. Everything else is speculation. Profile first, fix second. Always.

Here's the priority order that actually holds up in production. Fix data-fetching and state architecture first — bad patterns here cause orders-of-magnitude more re-renders than anything else. Then fix large list rendering (virtualise anything over 100 items; react-virtual from TanStack is the go-to in 2026). Then code-split heavy panels. Then and only then, look at component-level memo and useMemo for individual bottlenecks the Profiler surfaces.

# Performance triage order (2026 edition)
1. Data fetching — server state (TanStack Query), avoid waterfalls
2. State scope — don't put everything in global context
3. List virtualisation — TanStack Virtual for 100+ items
4. Code splitting — lazy() for routes + heavy panels
5. Compiler output — verify it's actually running (build logs)
6. Manual memo — only where Profiler shows > 2ms per render
7. CSS animations over JS animations — compositor thread
8. Bundle size — nothing over 50KB un-split (check with bundlesize or size-limit)

The compiler is a great tool, but treat it as a floor, not a ceiling. It raises the baseline performance of code you'd write anyway. The architectural decisions — where state lives, how data flows, what gets code-split — are still yours to make. And those decisions matter more in 2026 than they ever did, because UI complexity has scaled faster than hardware has. A well-structured React app with zero manual memoisation will outperform a badly structured app where every component is wrapped in memo. That's been true since 2019 and it's still true now.

For teams building with a component library, spend time understanding its rendering model before you commit. Browse the Empire UI library and check whether components use stable class names, whether animations are CSS-based, and whether there are obvious deep wrapper hierarchies. Five minutes of reading source code saves hours of Profiler debugging later.

FAQ

Does the React Compiler replace useMemo and useCallback entirely?

Not entirely. The compiler handles most derivations and pure render logic automatically, but you'll still write useMemo manually for expensive computations over large datasets, and useCallback for stable function references passed to components that initialise from props (canvas renderers, virtualised lists, external event emitters). The compiler skips anything it can't safely analyse.

When should I use React.memo in 2026?

When the Profiler shows a component taking more than 2ms to render and its props don't change every time the parent updates. Don't add it pre-emptively — the compiler already handles most cases, and adding explicit memo to compiler-processed components is redundant overhead. Measure first.

What's the fastest way to find which component is causing slowdowns?

Open React DevTools, switch to the Profiler tab, enable 'Record why each component rendered', trigger the slow interaction, then stop the recording. Click the slowest bar and read the 'Why did this render?' panel. It tells you exactly which prop or state change triggered the update — no guessing required.

Does choosing a UI component library affect React performance?

Yes, more than most developers expect. Libraries that compute styles at runtime (CSS-in-JS with dynamic styles) recompute on every render. Libraries built on static utility classes like Tailwind emit fixed class strings and have near-zero render overhead. Check the library's rendering model before committing to it, especially if you're rendering hundreds of component instances.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

React Performance in 2026: The 9 Optimizations That Actually WorkReact Compiler Beta: What It Auto-Optimizes and When You Still Need MemoReact UI Library Bundle Size Compared: shadcn, MUI, Mantine, Empire UIReact vs Svelte in 2026: Honest Comparison After 5 Years of SvelteKit