Back to blog
React
Intermediate

Mastering React Performance: Concurrent Mode and Suspense Guide

This article explores React Concurrent Mode and Suspense, explaining core concepts, architecture, and practical steps to improve performance. It includes real-world examples, production code, a comparison table, best practices, common mistakes, performance tips, security considerations, deployment notes, debugging tips, and an extensive FAQ.

September 26, 202520 min read

Introduction

React has evolved from a simple UI library into a comprehensive platform for building interactive applications. One of the most impactful advancements in recent years is the introduction of Concurrent Mode and Suspense. These features enable React to prepare multiple versions of the UI simultaneously, interrupt long-running renders, and prioritize urgent updates such as user input. The result is a more responsive user experience, especially in data‑intensive applications where network latency or heavy computation can block the main thread.This guide is aimed at intermediate to advanced React developers who want to move beyond the basics and harness the full power of concurrent rendering. We will explore the underlying concepts, examine the architecture that makes Concurrent Mode possible, walk through a step‑by‑step setup, illustrate real‑world usage patterns, provide production‑ready code snippets, compare related approaches, and cover best practices, common pitfalls, performance tips, security considerations, deployment notes, debugging strategies, and a detailed FAQ.By the end of this article you will have a clear mental model of how Concurrent Mode and Suspense work, know how to integrate them into existing projects, and be equipped to make informed decisions about when and how to adopt these advanced features.

Table of Contents

Core Concepts

Concurrent Mode is not a separate mode you toggle on or off; it is a set of capabilities that allow React to work on multiple tasks at once and yield control back to the browser when needed. The key ideas behind Concurrent Mode are time slicing, automatic batching, and prioritized rendering.

Time slicing enables React to split rendering work into small units and pause them to let the browser handle high‑priority events such as clicks or keystrokes. When the browser is idle, React resumes the paused work. This prevents long frames that cause jank.

Automatic batching groups multiple state updates that occur within the same event loop tick into a single re‑render, reducing unnecessary work. In Concurrent Mode, this batching is smarter and can span across asynchronous boundaries.

Prioritized rendering assigns different levels of urgency to updates. Synchronous events like user input get high priority, while data fetching or background updates receive lower priority. React can interrupt a low‑priority render to accommodate a high‑priority one, then resume the lower‑priority work later.

Suspense is a complementary feature that lets components "wait" for something before they render. The most common use case is data fetching, where a component suspends while a promise is pending and shows a fallback UI. Suspense works naturally with Concurrent Mode because the suspended component yields control, allowing other parts of the tree to continue rendering.

Together, these mechanisms enable a more fluid user experience. Instead of blocking the UI while a large list is being sorted or a large dataset is being fetched, React can keep the interface responsive and show meaningful placeholders.

Architecture Overview

Understanding the internal architecture helps you reason about behavior and debug issues. React's core consists of three main layers: the scheduler, the reconciler, and the renderer.

The scheduler is responsible for assigning priorities to units of work and deciding when to yield to the browser. It implements time slicing by breaking work into small chunks and using requestIdleCallback (or a polyfill) to schedule the next chunk when the browser is free.

The reconciler, often called "fiber", is the engine that performs the actual diffing of the component tree. In fiber, each unit of work is a fiber node that can be paused, resumed, or aborted. This incremental reconciler makes it possible to interrupt rendering and come back later.

The renderer takes the reconciled tree and commits changes to the DOM. In the browser, this is ReactDOM; in React Native, it is the native renderer. The commit phase is synchronous and cannot be interrupted, which is why React prepares all changes during the render phase and then applies them quickly.

Concurrent Mode leverages the fiber architecture to enable features like Suspense. When a component suspends, it throws a promise that is caught by the nearest Suspense boundary. The scheduler then marks the suspended subtree as low priority and continues working on other parts of the tree.

Another important concept is selective hydration. In server‑side rendering, HTML is sent to the client first, and then React hydrates the interactive parts. With Concurrent Mode, hydration can be prioritized based on user interactions, so the parts the user is interacting with become interactive first.

Step‑by‑Step Guide

To start using Concurrent Mode and Suspense in a React project, follow these steps:

  1. Ensure you are using React 18 or later, as Concurrent Mode is enabled by default in the root when using createRoot.
  2. Replace the legacy ReactDOM.render call with ReactDOM.createRoot. This activates concurrent features automatically.
  3. Identify parts of your UI that can benefit from concurrent rendering, such as large lists, heavy computations, or data‑fetching components.
  4. Wrap those components (or their parents) with <Suspense fallback=<Spinner />> to show a fallback while they are waiting for data.
  5. Implement data fetching that integrates with Suspense by throwing a promise when data is not ready. You can use a custom resource wrapper or a library like react‑query that supports Suspense.
  6. For fine‑grained control, use the startTransition API to mark updates as low priority, allowing urgent updates like user input to interrupt them.
  7. Test your application with the React Profiler to verify that long‑running renders are being split and that the UI remains responsive.
  8. If you are using server‑side rendering, consider using ReactDOMServer.renderToPipeableStream to enable selective hydration and streaming HTML.

Each step builds on the previous one, allowing you to adopt concurrent features incrementally without rewriting your entire application.

Real‑World Examples

Consider a dashboard that displays a large table of sales data, a chart that visualizes trends, and a sidebar with filters. Without Concurrent Mode, sorting the table or applying a filter could block the UI for several hundred milliseconds, causing a noticeable lag.

With Concurrent Mode, the sorting operation can be assigned a low priority. When the user types in the filter box, a high‑priority update interrupts the sorting, updates the filter state immediately, and then React resumes the sorting work in the background. The user experiences instant feedback while the heavy computation continues.

Another example is a profile page that fetches the user's posts, followers, and recent activity from three different endpoints. Using Suspense, each section can suspend independently while its data loads, showing a skeleton loader. Because the suspensions are independent, the sections that finish loading first can appear immediately, improving perceived performance.

In an e‑commerce product listing, imagine a search bar that filters results as the user types. Each keystroke triggers a filter operation that could be expensive if the product catalog is large. By wrapping the filter logic in a transition, the UI stays responsive: the input updates instantly, and the filtered results appear as soon as the low‑priority work completes.

Production Code Examples

Below is a realistic example of a Suspense‑enabled data‑fetching component that integrates with a custom resource wrapper.

import React, { useState, useEffect, startTransition } from 'react';// Simple resource wrapper that throws a promise while data is loadingfunction createResource(fetchFn) {  let status = 'idle';  let result;  let error;  const resource = () => {    if (status === 'pending') {      throw result; // promise    } else if (status === 'error') {      throw error;    } else if (status === 'success') {      return result;    }  };  const load = () => {    status = 'pending';    result = fetchFn(); // assume returns a promise    result.then(      r => {        status = 'success';        result = r;      },      e => {        status = 'error';        error = e;      }    );  };  return { resource, load };}// Usagefunction PostsTab() {  const postsResource = createResource(() => fetch('/api/posts').then(r => r.json()));  return (    

Posts

);}function PostsList({ resource }) { const posts = resource.resource(); // will suspend if not ready return (
    {posts.map(p => (
  • {p.title}
  • ))}
);}function App() { const [showPosts, setShowPosts] = useState(false); return (
{showPosts && ( Loading posts…
}> )}
);}// Root rendering with createRoot (React 18)import { createRoot } from 'react-dom/client';const container = document.getElementById('root');const root = createRoot(container);root.render();

The example demonstrates how a component can suspend while waiting for data, how startTransition marks a state update as low priority, and how the Suspense boundary shows a fallback. In a real application you would replace the simple fetch wrapper with a more robust caching solution, but the core pattern remains the same.

Comparison Table

FeatureConcurrent Mode (React 18)Legacy Mode (React 17 and earlier)Notes
Rendering InterruptionYes – low priority work can be yieldedNo – rendering is synchronous and cannot be interruptedEnables responsiveness
Automatic BatchingYes – includes async boundariesLimited – only batches synchronous eventsReduces unnecessary renders
Suspense IntegrationNative – works with data fetching, lazy loading, etc.Partial – only works for lazy loading via React.lazyBroader use cases
startTransition APIAvailableNot availableAllows marking updates as low priority
Server‑Side Rendering HydrationSelective hydration, streaming HTMLFull hydration after all HTML receivedImproves time to interactive
Bundle Size ImpactNegligible (feature flags)Same baselineNo extra cost for enabling

The table highlights the key differences between Concurrent Mode and the legacy synchronous rendering model. The advantages are most noticeable in applications with heavy rendering work or asynchronous data flows.

Best Practices

  • Always use createRoot to enable Concurrent Mode; do not mix legacy ReactDOM.render with concurrent features.
  • Wrap asynchronous boundaries with <Suspense> and provide a meaningful fallback UI (skeleton loaders, placeholders).
  • Use startTransition for updates that can be deferred, such as filtering large lists or initiating non‑critical data fetches.
  • Avoid wrapping synchronous user input updates in transitions; they should remain high priority to feel instant.
  • Leverage selective hydration when using server‑side rendering to prioritize the parts of the page the user interacts with first.
  • Test with the React Profiler and enable the "Highlight updates when components render" option to visualize rendering bursts.
  • Keep components pure and side‑effect free during render; Suspense relies on the ability to retry renders.
  • When building custom resources for Suspense, ensure the thrown promise is identical each time until resolved; otherwise React may create infinite loops.
  • Do not overuse transitions; reserve them for work that is truly interruptible to avoid unnecessary complexity.
  • Stay up to date with React releases; the concurrent features evolve and may introduce new APIs or deprecations.

Common Mistakes

  • Using legacy ReactDOM.render while expecting Suspense to work – Suspense boundaries will not capture promises.
  • Putting a startTransition around a state update that triggers a synchronous user action, causing delayed feedback.
  • Failing to provide a fallback for Suspense, resulting in a blank UI while data loads.
  • Throwing different promises on each render in a custom resource, causing infinite suspend‑resolve loops.
  • Neglecting to error‑handle suspended promises; errors should be caught by the nearest error boundary or cause the Suspense boundary to show an error fallback.
  • Assuming Concurrent Mode eliminates the need for memoization; heavy computations still benefit from useMemo and useCallback.
  • Over‑splitting components into tiny Suspense boundaries, which can increase overhead without gain.
  • Ignoring server‑side rendering considerations; streaming HTML requires adjustments to data fetching on the server.
  • Using Concurrent Mode features in React Native without verifying compatibility; some APIs differ.
  • Believing that Concurrent Mode will automatically fix all performance issues; it is a tool, not a silver bullet.

Performance Tips

  • Profile your application with the Chrome DevTools Performance panel to identify long tasks.
  • Move expensive calculations out of render phase into web workers or useInteraction hooks when available.
  • Use useTransition for UI‑state updates that can defer, such as toggling a sidebar or changing a theme.
  • Implement virtualized lists (e.g., react‑window) for large datasets to reduce the number of DOM nodes.
  • Leverage React's automatic batching to reduce redundant renders; avoid manual flushSync unless absolutely necessary.
  • When using Suspense for data fetching, consider a caching layer (like React Query) to prevent duplicate requests.
  • Enable the react‑debug‑tools profiler to measure commit and render times.
  • If you notice frequent suspensions, audit your data‑fetching waterfall; try to fetch data in parallel where possible.
  • Keep the size of your JavaScript bundles small; large bundles increase parse time and can offset gains from concurrent rendering.
  • Use the react‑18‑strict‑mode to detect unsafe lifecycle methods that could break concurrent rendering.

Security Considerations

  • Concurrent Mode does not introduce new security vulnerabilities, but the asynchronous nature can expose timing‑based attacks if sensitive data is inadvertently exposed in fallback UI.
  • Ensure that fallback UIs do not leak privileged information; show only generic placeholders.
  • When using startTransition, be aware that low‑priority state updates may be delayed; avoid using them for security‑critical state such as authentication tokens.
  • Validate and sanitize all data fetched from external sources before rendering, regardless of rendering mode.
  • Keep dependencies up to date; known issues in older React versions could affect concurrent rendering safety.
  • Review your Content Security Policy (CSP) to ensure inline scripts are not inadvertently allowed due to dynamic imports used with Suspense.

Deployment Notes

  • When deploying to a CDN, ensure that the HTML generated by server‑side rendering includes the correct id attributes for hydration targets.
  • If you are using edge middleware or serverless functions, verify that the streaming API (renderToPipeableStream) is supported in your runtime.
  • Set appropriate cache‑control headers for static assets; React's concurrent features rely on fast JavaScript parsing.
  • Monitor server response times; slow APIs increase suspension duration and can degrade perceived performance.
  • Consider using a feature flag to roll out Concurrent Mode gradually, especially if you have a large user base with older devices.
  • Check that your production error tracking captures errors that occur during suspension or transition periods.

Debugging Tips

  • Use the React Developer Tools profiler to record interactions and examine which components suspended and for how long.
  • Enable the "Highlight updates when components render" option to see rendering bursts caused by transitions.
  • If a component suspends indefinitely, verify that the promise thrown is being resolved; network errors or logic bugs can leave promises pending.
  • Look for warnings in the console about "Maximum depth exceeded" which can indicate a resource that re‑throws a new promise each render.
  • When using startTransition, check that the UI updates you expect to be deferred actually appear in the profiler as low‑priority work.
  • In server‑side rendering, ensure that the data needed for the initial HTML is fetched before streaming; otherwise the client will suspend immediately.
  • Test on devices with limited CPU to see how time slicing affects frame rates.
  • Use the react‑18‑strict‑mode component in development to catch unsafe concurrent‑mode usage.
  • If you notice excessive garbage collection, audit the size of objects thrown as promises; keep them lightweight.

FAQ

What is Concurrent Mode in React?

Concurrent Mode is a set of features in React 18 that allows React to prepare multiple versions of the UI simultaneously, interrupt long‑running renders, and prioritize urgent updates such as user input, resulting in a more responsive user experience.

Do I need to rewrite my entire app to use Concurrent Mode?

No. You can enable Concurrent Mode at the root by switching to createRoot and then gradually adopt Suspense and transitions in parts of your application where they provide the most benefit.

How does Suspense work with data fetching?

A component can "suspend" by throwing a promise while waiting for data. The nearest <Suspense> boundary catches the promise and shows its fallback UI. When the promise resolves, React retries rendering the component with the resolved data.

What is the difference between useTransition and startTransition?

startTransition is a function that marks a state update as low priority. useTransition is a hook that returns [startTransition, isPending] where isPending is a boolean indicating whether a transition is currently active.

Can I use Concurrent Mode with React Native?

React Native has its own renderer and does not yet fully support the web‑focused Concurrent Mode APIs. Some concepts like time slicing are present, but the Suspense and transition APIs may differ or be unavailable.

Does Concurrent Mode eliminate the need for memoization?

No. While Concurrent Mode can interrupt rendering, expensive computations still benefit from useMemo, useCallback, and React.memo

What happens if a suspended promise rejects?

The rejection is treated like an error and will be caught by the nearest error boundary. If no error boundary exists, the error will propagate and may cause the application to crash.

How does selective hydration improve SSR?

Selective hydration allows React to hydrate parts of the component tree based on user interactions. The HTML is streamed from the server, and the client prioritizes hydrating the parts the user is interacting with first, reducing time to interactive.

Is there a performance downside to enabling Concurrent Mode?

Enabling Concurrent Mode adds a negligible overhead. In most cases the benefits outweigh the cost. Poorly written components that throw new promises on each render can cause infinite suspensions, so correct implementation is essential.

Can I use third‑party state‑management libraries with Concurrent Mode?

Many popular libraries (Redux Toolkit, React Query, Zustand, etc.) have added support for Concurrent Mode and Suspense. Check the library's documentation for specific usage guidelines.

Conclusion

React Concurrent Mode and Suspense represent a significant step forward in building responsive, user‑centric applications. By allowing React to yield control, prioritize urgent updates, and show meaningful placeholders while data loads, these features help eliminate jank and improve perceived performance.

Adopting Concurrent Mode does not require a complete rewrite. Start by switching to createRoot, identify UI sections that block the main thread, wrap them with Suspense, and use startTransition for low‑priority state updates. Follow the best practices outlined above, avoid the common pitfalls, and continuously monitor performance with the React Profiler and browser dev tools.

As the React ecosystem continues to evolve, more libraries and tools will embrace concurrent patterns. Investing time now to understand and apply these concepts will pay off in smoother user experiences, better engagement, and a stronger foundation for future features.

Take the next step: audit your current project for blocking updates, implement a Suspense‑bounded data‑fetcher for one of your routes, and measure the difference in interaction latency. Share your findings with your team and iterate toward a more concurrent‑ready codebase.