DEV Community

Cover image for How React Works (Part 1)?Motivation Behind React Fiber: Time Slicing & Suspense
Sam Abaasi
Sam Abaasi

Posted on

How React Works (Part 1)?Motivation Behind React Fiber: Time Slicing & Suspense

Motivation Behind React Fiber: Time Slicing & Suspense

Series: How React Works Under the Hood
Level: Intermediate to Advanced
Prerequisites: Basic React knowledge. Bonus: if you've read How JavaScript Works — you'll feel right at home.


Introduction

You've used React. You've felt that lag — the input that stutters, the page that freezes for half a second, the animation that just doesn't feel right. But have you ever wondered why React had to completely rewrite its entire internal engine just to fix it?

In 2017, the React team announced React Fiber — a ground-up rewrite of React's internals. Not the API. Your JSX and components stayed the same. But the engine underneath, the thing that actually decides how and when work gets done, was thrown out and rebuilt from scratch.

To understand why Fiber was necessary — not just useful, but necessary — we need to start where the React team started. This entire article is grounded in Dan Abramov's talk at JSConf Iceland 2018. If you want to watch it alongside reading, here it is:

🎬 Dan Abramov — Beyond React 16 | JSConf Iceland 2018

Now let's get into it.


⚡ The Core Question

"With vast differences in computing power and network speed, how do we deliver the best user experience for everyone?"

This isn't a warm-up. It's the exact design challenge the React team was solving. Think about who uses your app:

  • A developer on a MacBook Pro with fiber internet
  • A user in rural Southeast Asia on a 3-year-old Android phone with a 3G connection
  • Someone on a mid-range laptop on a crowded café WiFi

React runs on all of these. And in 2017, it was doing a poor job of adapting to any of them. The problems fell into two distinct categories.

CPU/IO → solutions → Fiber


🖥️ CPU Problems — The Cost of Rendering

These are problems caused by expensive computation. When React updates your UI, it has to do real work: creating DOM nodes, calling render functions, reconciling old and new trees, applying changes to the DOM. On a powerful machine, this might take 5ms. On a low-powered mobile device, the exact same work might take 50ms or more.

CPU problems look like:

  • A complex chart re-rendering while you're typing
  • Mounting a large component tree on initial load
  • Re-rendering a long list when one item changes

🌐 IO Problems — The Cost of Waiting

These are problems caused by time spent waiting for things to arrive — data from an API, a JavaScript bundle from a CDN, an image. The code is fine. The device is fine. You're just waiting on the network.

IO problems look like:

  • Data-fetching waterfalls — fetch A, then B, then C, each blocking the next
  • Code splitting — loading a bundle before a page can render
  • Showing wrong loading states because data arrived faster or slower than expected

Both needed new solutions. And as we'll see, both solutions required the exact same capability from React's engine. But first — let's see the problem in action.


🎬 The Demo That Changed Everything

The best way to feel this problem is to see it. Jump to 2:57 in Dan's talk — this is the moment the demo lands.

The app: an input field at the top, a detailed chart below. The rule: the more characters you type, the more detailed the chart gets. Both updates — the input and the chart — happen at the same time.

The Demo That Changed Everything

Watch the input. There's a clear delay between pressing a key and seeing the character appear. The browser is so busy computing the chart that it can't process keystrokes fast enough. The thread is blocked for the entire duration of the render — which might be 50, 80, even 100ms — and the user feels every millisecond of it.

Dan put the root problem precisely:

"The underlying problem is that the update is big and synchronous — once React starts rendering, it cannot stop."

This is visible directly in the React source code. In synchronous mode, the work loop looks like this:

// From React source — synchronous mode
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
Enter fullscreen mode Exit fullscreen mode

workLoopSync vs workLoopConcurrent

It's a plain while loop with no exit condition other than finishing everything. Once React starts, it runs to completion. There is no way to pause it from the outside.


🩹 The Attempted Fix: Debouncing

The most natural workaround: instead of updating the chart on every keystroke, wait until the user pauses typing, then do one big update.

debounced version — input is responsive but chart lags, feels disconnected

Better? A little. But it has three real problems.

1. It's not adaptive to the device. Debouncing uses a fixed delay — say 300ms. If the user's machine is powerful enough to render instantly, they still wait 300ms. If it's slow, 300ms might not even be enough time for the render to finish without freezing.

2. It still locks up when it fires. Try enabling CPU throttling in DevTools (4–6x slowdown, simulating a low-end phone). Even with debouncing, the moment the update fires, the browser locks up completely. You've moved the jank — not eliminated it.

3. It doesn't help with mounting. Debouncing can delay an update, but when you first mount a large component tree, there's nothing to debounce. React mounts it all at once, synchronously. During that time, nothing in the page is interactive — click events don't register, animations freeze.

The problem isn't when the update fires. It's that the update is synchronous and uninterruptible when it does.


💡 What If React Could Pause?

What if React could start rendering the chart update, notice that a keystroke arrived, pause the render, handle the keystroke immediately, then resume from exactly where it stopped?

The total amount of work is identical. But the experience is completely different — because users get immediate feedback on their input while the expensive work finishes quietly in the background.

To understand why this requires a completely new engine, we need to talk about frames.


🎞️ The 16ms Frame Budget

Browsers aim to run at 60 frames per second. The math:

1000ms ÷ 60 frames ≈ 16.67ms per frame
Enter fullscreen mode Exit fullscreen mode

Every ~16ms, the browser needs to execute JavaScript, recalculate styles, lay out the page, and paint pixels to the screen. If JavaScript takes more than ~16ms in a continuous burst, the browser misses that frame. You see jank — stuttering, freezing, lag.

frame budget — one long sync task drops frames vs sliced work fits each frame

Old React had no awareness of this budget at all. A single render could take 100ms and React would march through it, never yielding control, never letting the browser breathe.


⏱️ Time Slicing

Time Slicing is React's solution to the CPU problem.

The core idea: instead of doing all rendering in one continuous chunk, React slices that work into small pieces and checks between each piece whether something more urgent has arrived. This is exactly what the concurrent version of the work loop does differently:

// From React source — concurrent mode
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
Enter fullscreen mode Exit fullscreen mode

Spot the difference from workLoopSync: !shouldYield(). After every single unit of work, React asks the Scheduler: "Is it time to yield?"

Here's the actual shouldYield logic from the React Scheduler source:

function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false; // still have budget — keep working
  }
  return true; // time's up — yield to the browser
}
Enter fullscreen mode Exit fullscreen mode

Each task is given about 5ms. When that time is up, React pauses. The browser gets a chance to handle user input and paint. Then React resumes from exactly where it left off.

The result:

async version — input updates instantly on every keystroke, chart renders gradually in background

The input updates immediately on every keystroke. The chart still renders — it just does so across multiple frames instead of one big block. The total work is the same. The experience is night and day.

Dan described the properties precisely at JSConf Iceland:

"We've built a generic way to ensure that high-priority updates don't get blocked by a low-priority update, called time slicing. If my device is fast enough, it feels almost like it's synchronous; if my device is slow, the app still feels responsive. It adapts to the device thanks to the requestIdleCallback API. Notice that only the final state was displayed — the rendered screen is always consistent and we don't see visual artifacts of slow rendering causing a janky user experience."

Three properties to lock in:

  • Feels synchronous on fast devices — slices are so small and fast you never notice them
  • Feels responsive on slow devices — high-priority work (keystrokes) always gets through
  • Only the final state is shown — no intermediate render states flash on screen

🌿 The Git Metaphor

Dan used a metaphor in his talk that makes prioritization intuitive.

Without time slicing, React is like working directly on main. An urgent bug comes in mid-feature — you can't fix it. You have to finish the feature first.

With time slicing, React works like a proper branching workflow. You start on a feature branch — the low-priority chart render. An urgent task arrives — a keystroke. You handle it on main first, then rebase your feature branch on top when you're done.

React does the rebasing automatically. The low-priority render continues from where it stopped, now applied on top of the most current state. You never think about it.

Git branch metaphor — sync vs async prioritization

⏳ Suspense

If Time Slicing solves the CPU side, Suspense solves the IO side.

Dan introduced it this way:

"We've built a generic way for components to suspend rendering while they load async data, which we call suspense. You can pause any state update until the data is ready, and you can add async loading to any component deep in the tree without plumbing all the props and state through your app and hoisting the logic."

The concept: instead of manually managing loading states with isLoading flags and scattered useEffect fetches, a component can declare that it's waiting and React handles the rest.

function MovieDetails({ id }) {
  // If data isn't cached yet, this suspends — throws a Promise
  const movie = movieCache.read(id);
  return <div>{movie.title}</div>;
}

<Suspense fallback={<Spinner />}>
  <MovieDetails id={42} />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

Internally, when a component suspends, it throws a Promise. React catches it at the nearest <Suspense> boundary, shows the fallback, and retries the render when the Promise resolves. We'll go deep on the exact mechanics — including how this connects to algebraic effects — in Part 4.

Suspense mechanism — throw Promise, catch at boundary, retry on resolve

What's powerful is how it adapts to network conditions:

"On a fast network, updates appear very fluid and instantaneous without a jarring cascade of spinners that appear and disappear. On a slow network, you can intentionally design which loading states the user should see and how granular or coarse they should be, instead of showing spinners based on how the code is written. The app stays responsive throughout."

And the key point Dan closed with:

Suspense demo — navigating the app while a subtree loads in background, no spinner waterfall

"Importantly, this is still the React you know. This is still the declarative component paradigm that you probably like about React."

The mental model for you as a developer doesn't change. The engine underneath does.


🔗 The Common Thread: Interrupting

Here's what ties both features together.

Time Slicing and Suspense look completely different — one is about CPU, one is about IO. But they share one fundamental requirement:

React needs to be able to interrupt what it's doing and resume it later.

  • Time Slicing: interrupt to yield within the 16ms frame budget
  • Suspense: interrupt to wait for async data

Same capability. This is exactly why React needed Fiber.

The old React engine used JavaScript's native call stack to track all rendering work. And the native call stack has a hard limitation: you cannot pause it from the outside. Once a stack of function calls starts executing, it runs to completion. There is no JavaScript API to say "stop here, do something else, resume from this exact frame."

Matheus Albuquerque explained the solution beautifully in his React Summit 2022 talk:

"We can think of the Fiber architecture as a React-specific call stack model that gives full control over scheduling of what should be done. And a Fiber itself is basically a stack frame for a given React component."

And crucially — unlike native stack frames, Fibers are just plain JavaScript objects. React can create them, inspect them, pause them, delete them, and resume them whenever it wants. Sam Galson put the key insight in one sentence:

"Fiber is a reimplementation of the stack, specialized for React components. The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them however — and whenever — you want."

native JS call stack (opaque, uninterruptible

This gives us React's internal pipeline, as described by jser.dev:

  • Trigger — something changes (setState, event). React registers the work.
  • Schedule — the Scheduler, a priority queue, decides when and in what order work happens.
  • Render — React walks the Fiber tree and figures out what changed. This phase is interruptible. This is where Time Slicing and Suspense live.
  • Commit — React applies changes to the real DOM. Synchronous. Cannot be interrupted.

React 4 phases — Trigger → Schedule → Render → Commit

Fiber makes the Render phase interruptible. Everything else follows from that.


🗺️ What's Coming in This Series

This was the why. The rest of the series is the how — each part built around one problem and one solution.

Part Title Core story
Part 2 Why React Had to Build Its Own Execution Engine JS call stack can't pause → React builds Fiber + Scheduler
Part 3 How React Finds What Actually Changed Re-rendering everything is too slow → the Reconciler + keys
Part 4 The Idea That Makes Suspense Possible Algebraic effects → how Suspense and ErrorBoundary actually work
Part 5 The React Lifecycle From the Inside When useEffect and useLayoutEffect fire and why
Part 6 How State Actually Works useState, useRef, dispatch, and the commit state phase
Part 7 The Trap of Vibe Coding useCallback What causes re-renders — before reaching for memo and useCallback
Part 8 Server Components & Hydration: The Real Story How React moved rendering to the server — and what it cost
Part 9 The Complete Picture: From a Keystroke to a Pixel Every layer of React working together as one system

🎬 Watch These Talks

This article is a distillation. The primary sources are worth your time — here's what to watch and why:

Dan Abramov — Beyond React 16 | JSConf Iceland 2018 (33 min)
The talk this entire article is based on. Key moments: the demo at 2:57, the async version revealed at 6:10, the Git metaphor at 8:16, and the Suspense demo from 15:40.

Matheus Albuquerque — Inside Fiber: the In-Depth Overview | React Summit 2022 (27 min)
The deepest walkthrough of FiberNode internals I've found. Start at 2:11 for the Fiber-as-call-stack explanation that directly connects to Part 2 of this series.

Sam Galson — Magic in the web: coroutines, continuations, fibers | React Advanced London (~30 min)
The computer science behind React's approach — coroutines, continuations, algebraic effects, and why Fiber is the shape it is. Essential background for Parts 2 and 3.


🙏 Sources & Thanks

A huge thank you to everyone whose work made this article possible:

  • Dan Abramov — for Beyond React 16 and for the Git metaphor that makes Time Slicing click
  • Matheus Albuquerque — for the clearest explanation of FiberNode internals at React Summit 2022. Transcript via gitnation.com
  • Sam Galson — for connecting React to the wider CS theory of fibers, coroutines, and continuations. Full article: Continuations, coroutines, fibers, effects (YLD Blog)
  • Sophie Alpert & the React teamSneak Peek: Beyond React 16 (React Blog) — the official writeup with Dan's exact quotes
  • jser.dev — the best source-level analysis of React internals on the internet. The workLoopSync, workLoopConcurrent, shouldYieldToHost, and the 4-phase model in this article all come from JSer's series — especially the overview, the scheduler breakdown, and the Lanes deep dive
  • Lydia Hallie — for in-row and non-in-row fetching explanations and for the JavaScript visualizations that shaped this series' style

Part 2 is next — JavaScript's call stack is why React couldn't do any of this. Here's what React built instead. 🔧


Tags: #react #javascript #webdev #tutorial

Top comments (0)