React 18 Concurrent Rendering Explained: A Practical Guide for Modern Developers

Discover how React 18 keeps your UI fast and responsive with concurrent rendering, automatic batching, and smart scheduling.

Concurrent Rendering

React 18 introduced one of the most important architectural upgrades in the framework’s history: concurrent rendering. If you’ve ever faced UI freezes during heavy updates or struggled with unnecessary re-renders, this update directly addresses those challenges.

In this article, we’ll break down how concurrent rendering works, what powers it under the hood, and how you can use it effectively in real-world applications.

👉 To read more articles on React visit here

The Big Shift: From Blocking to Interruptible Rendering

Before React 18, rendering was mostly synchronous. Once React started updating the UI, it would finish the entire task before handling anything else. In simple apps, that worked fine. But in complex dashboards or data-heavy interfaces, this could block user interactions.

With React 18, rendering can now be paused, resumed, or even abandoned if something more urgent happens — like a user click. This is possible thanks to the Fiber reconciler, the engine behind React’s new rendering model.

Fiber Reconciler: The Engine Behind Concurrent Rendering

The React Fiber reconciler breaks rendering into small units of work called fibers. Each fiber represents a component and stores information such as state, props, and side effects.

React maintains two trees:

  • Current tree — What’s currently displayed on screen.
  • Work-in-progress (WIP) tree — A draft version where updates are prepared.

What is the WIP Fiber Tree?

Think of the WIP tree as React’s “draft workspace.” During the render phase, React clones the current tree and applies new updates inside this WIP version. This work happens in memory — no DOM updates yet.

Because this phase is interruptible:

  • React can pause work.
  • Resume it later.
  • Or discard it entirely if a higher-priority update arrives.

Once the rendering work is complete, React swaps the WIP tree with the current tree and applies changes in one atomic step. This double-buffering technique prevents flickers and ensures users only see fully completed updates.

Automatic Batching: Fewer Renders, Better Performance

Another major improvement in React 18 is automatic batching.

In earlier versions, React only grouped multiple state updates if they happened inside React event handlers. Updates inside setTimeout, promises, or async functions triggered separate re-renders.

Now, React batches all updates — no matter where they originate.

Example Scenario

In React 17:

setTimeout(() => {
setCount(c => c + 1);
setFlag(true);
});

This would cause two renders.

In React 18, the same code results in just one render.

For data-intensive apps, this can significantly reduce render counts — sometimes by 40% or more — improving frame rates and responsiveness under load.

Smart Scheduling with Lanes

Concurrent rendering isn’t just about interrupting work. It’s also about assigning priority.

React uses a 31-lane priority system called Lanes. Each lane represents a different urgency level. Internally, these lanes are managed efficiently using bitwise operations.

Here’s a simplified view of priority levels:

  • SyncLane — Immediate updates (e.g., error handling)
  • InputContinuousLane — User interactions like typing
  • DefaultLane — Standard updates
  • TransitionLane — Non-urgent transitions
  • IdleLane — Background work

The scheduler processes tasks in small chunks (about 5 milliseconds at a time) and yields control back to the browser to keep interactions smooth. If an update waits too long, React can promote it to a higher priority to prevent starvation.

Controlling Priority with useTransition and useDeferredValue

React 18 also gives developers control over update urgency.

useTransition

Use this when you want to mark certain updates as non-urgent — like filtering a large list.

const [isPending, startTransition] = useTransition();
startTransition(() => {
setFilteredItems(
items.filter(item => item.includes(query))
);
});

While the transition runs, isPending becomes true, allowing you to show a loading indicator. If a user types again, React can interrupt the filtering work to keep input responsive.

useDeferredValue

This hook creates a delayed version of a value. It’s helpful when rendering based on that value is expensive.

const deferredQuery = useDeferredValue(query);

React updates deferredQuery after prioritising urgent interactions, helping avoid UI lag during fast typing.

Render vs Commit: Two Distinct Phases

Concurrent rendering separates work into two phases:

1. Render Phase (Interruptible)

  • Builds the WIP fiber tree.
  • Happens in memory.
  • Can be paused or abandoned.
  • No DOM changes occur.

2. Commit Phase (Synchronous)

  • Applies DOM updates.
  • Runs effects (useEffect, useLayoutEffect).
  • Cannot be interrupted.

If a render is discarded, its side effects are never executed. This prevents outdated logic from running.

Enabling Concurrent Rendering

Concurrent features are not automatic — you must opt in using createRoot.

import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<App />);

Using the legacy render() API keeps your app in synchronous mode and disables concurrent features like automatic batching and transitions.

Real-World Performance Gains

In complex applications such as dashboards or analytics platforms, concurrent rendering delivers measurable improvements:

  • Render counts reduced by up to 50–80% for async updates.
  • Frame drops during heavy updates significantly decreased.
  • Noticeably smoother input responsiveness.
  • Faster time-to-interactive under load.

These improvements are especially visible in apps with large datasets or frequent state changes.

Error Handling and Edge Cases

Concurrent rendering introduces some nuances:

  • Error boundaries still work, but partially rendered trees may be discarded.
  • Effects from abandoned renders do not execute.
  • Stale closures or race conditions can appear if effects aren’t written carefully.
  • Some third-party state libraries may not fully support concurrent rendering.
  • Strict Mode may trigger double renders in development to expose side effects.

Best practices include:

  • Placing error boundaries at logical UI sections.
  • Avoiding side effects during rendering.
  • Testing with profiling tools to identify priority issues.

Limitations and Unknowns

Some internal details remain undocumented:

  • The exact mechanics of fiber interruption and resumption.
  • The full internal mapping of all 31 lanes.
  • Scheduler implementation specifics.
  • Memory handling strategies for multiple WIP trees.

For most application developers, these details are not necessary. However, they may matter for advanced debugging or library authors.

Key Takeaways

  • React 18 introduces interruptible, priority-aware rendering.
  • The Fiber reconciler enables rendering in small, resumable units.
  • Automatic batching reduces unnecessary re-renders across all update sources.
  • A 31-lane priority system ensures urgent interactions stay responsive.
  • useTransition and useDeferredValue help fine-tune update urgency.
  • Rendering is split into interruptible render and synchronous commit phases.
  • Concurrent features require createRoot() to activate.

React 18 represents a fundamental shift from blocking updates to intelligent scheduling. For developers building modern, data-rich applications, understanding concurrent rendering is no longer optional — it’s essential for delivering fast, smooth user experiences.

Your Turn

What’s the next cool thing you want to learn about React?

If this blog helped you, let me know in the comments…
Your words might seem small, but they’re the reason I keep writing more🤗

At Dev Simplified, We Value Your Feedback 📊

👉 To read more articles on React visit here

👉 Follow us to not miss any updates.

👉 Have any suggestions? Let us know in the comments!

👉 Subscribe for free and join our growing community!