Explanation of React Fiber

Introduction

React Fiber is the core reconciliation engine of React supported since version 16, helping to change the rendering mechanism from synchronous to asynchronous.

Advantages:

  • Ability to pause and resume work: Allows React to break the rendering process into small units of work (fibers) so as not to block the main thread.
  • Task prioritization: Prioritizes direct user interactions (keystrokes, clicks) over background data processing tasks.
  • UX Improvement: Minimizes lag (jank) when the application has to handle complex UI components or large data lists.

Detailed Workflow

  • React Rendering Pipeline
    • Trigger Phase
      • When you call setState() or dispatchAction, React marks the changed nodes as dirty and uses the lane mechanism to mark upwards to the corresponding parent Fiber Nodes.
      • Fiber: Acts as a "unit of work." Each component is a Fiber node containing information about state, props and links to parent/child/sibling nodes.
    • Render Phase - Calculation
      • This is where Reconciliation and Fiber demonstrate their greatest roles.
      • Scheduling: React's Scheduler considers the priority of the update (High priority like mouse clicks or Low priority like fetching data).
      • Fiber Tree Traversal: React starts traversing from the Root to create a new tree called the workInProgress tree.
      • Thanks to dirty marking combined with lanes from the Trigger phase, React will Bail Out nodes that are not marked; only marked fiber nodes are checked and rendered for comparison.
      • Virtual DOM & Diffing: React runs the render logic of components to create a new Virtual DOM. Then, it compares (Diffing) this new V-DOM tree with the current V-DOM tree to find the list of necessary changes (e.g., needing to add a  tag, deleting a  tag).
      • Characteristics: This phase is entirely asynchronous. React can work a bit, pause to let the browser paint the UI (thanks to the Scheduler using a custom polyfill similar to requestIdleCallback), then return to continue without hanging the browser.
    • Commit Phase - Execution
      • After having the list of changes ("Effect List"), React moves to the Commit phase.
      • Real DOM Update: React directly intervenes in the real DOM (via appendChild, removeChild, setAttribute,...).
      • Tree Swapping (Double Buffering): After the update is complete, React simply swaps: the workInProgress tree now becomes the current tree.
      • Characteristics: This phase occurs synchronously to ensure users do not see a partial UI state.
  • Browser Rendering Pipeline: after React finishes the Commit Phase, React's job is essentially done, but the user hasn't seen anything yet. At this point, the Browser Rendering Pipeline begins to run to draw those changes on the screen:
    1. Style: Calculate CSS for elements.
    2. Layout: Determine the size and position of each node on the screen (often called Reflow).
    3. Paint: Color the pixels (into layers).
    4. Composite: Combine layers together based on z-index to display the final image on the screen.


Priority

Below are the priority levels from highest to lowest in the scheduler package:

  1. Immediate Priority (Level 1)
    • Behavior: This task will block the main thread until completion.
    • Usage: Used for extremely urgent situations needing immediate response so users don't feel the app is "frozen" at the moment of physical interaction.
  2. User Blocking Priority (Level 2)
    • Behavior: Very high priority. If this task is not performed, it will be elevated to "Immediate" level to force browser processing.
    • Usage: Usually associated with Discrete Events (click, keystroke, drag and drop). This level helps the UI respond quickly to user intent.
  3. Normal Priority (Level 3)
    • Behavior: This is the default priority for most state updates in React.
    • Usage: Work such as data fetching, rendering normal lists will reside here. Fiber can interrupt and rest freely at this level before it becomes urgent.
  4. Low Priority (Level 4)
    • Behavior: Low priority. Only performed when tasks at Normal level are finished or when waiting too long.
    • Usage: Analytical tasks (analytics), data logging, or rendering "nice to have" components that don't need to appear immediately.
  5. Idle Priority (Level 5)
    • Expiration time: Never expires (theoretically uses a very large number as the wait time).
    • Behavior: Only runs when the browser is truly "completely idle."
    • Usage: Memory cleanup tasks, background cache preparation for subsequent screens the user hasn't seen yet.


You can see specifically in the React code here which defines the specific expiration time values for each level. However, these values may be adjusted in future versions if issues arise, so you should only use them for reference purposes.

SchedulerFeatureFlags.js

export const userBlockingPriorityTimeout = 250;
export const normalPriorityTimeout = 5000;
export const lowPriorityTimeout = 10000;


Scheduler.js

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;

....

switch (priorityLevel) {
case ImmediatePriority:
// Times out immediately
timeout = -1;
break;
case UserBlockingPriority:
// Eventually times out
timeout = userBlockingPriorityTimeout;
break;
case IdlePriority:
// Never times out
timeout = maxSigned31BitInt;
break;
case LowPriority:
// Eventually times out
timeout = lowPriorityTimeout;
break;
case NormalPriority:
default:
// Eventually times out
timeout = normalPriorityTimeout;
break;
}


Detail

Here I will provide examples demonstrating React Fiber's operation in practice.

First, let's create the UseDefault.tsx file to simulate a heavy rendering issue.

import { SearchOutlined } from "@ant-design/icons";
import { Card, Input, Space, Table, Typography } from "antd";
import { useState } from "react";

const { Title, Text } = Typography;
const LIST = Array.from({ length: 10_000 }, (_, i) => i);

export function UseDefault() {
const [filterTerm, setFilterTerm] = useState("");
const [filteredList, setFilteredList] = useState<string[]>([]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterTerm(value);

const filtered = LIST.map(i => `Result for "${value.toLowerCase()}" - Entry #${i + 1}`);
setFilteredList(filtered);
};

const columns = [
{
title: "Item Name",
dataIndex: "name",
key: "name",
},
];

const dataSource = filteredList.map((name, index) => ({ key: index, name }));

return (
<div className="min-h-screen bg-gray-50 p-8 flex justify-center">
<Card className="w-full max-w-2xl shadow-lg border-0">
<Space orientation="vertical" size="large" className="w-full">
<header className="border-b pb-4">
<Title level={2} className="!mb-1">
Default behavior
</Title>
<Text type="secondary">No React Fiber, just filtering items on main thread</Text>
</header>

<section>
<Input size="large" placeholder="Search items..." prefix={<SearchOutlined className="text-gray-400" />} value={filterTerm} onChange={handleChange} allowClear />
</section>

<section className="border border-gray-100 rounded-lg overflow-hidden">
<Table
dataSource={dataSource}
columns={columns}
pagination={false}
scroll={{ y: 400 }}
virtual
size="middle"
showHeader={false}
locale={{ emptyText: filterTerm ? "No results" : "Type to search" }}
/>
</section>
</Space>
</Card>
</div>
);
}

This code demonstrates React's default rendering behavior. When a user enters text into the search box, the handleChange function updates filterTerm and performs data filtering immediately. With 10,000 records, processing and updating filteredList happens synchronously on the main thread. As a result, if the table rendering is too heavy, the Input box may stutter and not respond smoothly to the user's typing.


To solve the issue, we will use useTransition and useDeferredValue, both part of React's Concurrent Rendering suite to prioritize UI responsiveness.

  • useTransition is a hook that allows you to mark a state update as "non-urgent."
    • It is an Action-based control function.
    • Use it when you are the one initiating the change (e.g., writing onClick, onChange functions) and have access to the setState function.
    • Pending state (isPending): Provides a boolean variable to help developers display loading indicators or professionally blur old content.
    • Delaying state changes helps keep the UI responsive: Users can still interact with other parts of the page (like input) while React is calculating the render in the background.
    • Avoids UI "freezing": Prevents heavy state updates from stalling the browser.
  • useDeferredValue allows you to defer re-rendering non-urgent updates until the browser is idle, keeping the UI responsive during heavy computations.
    • It is a Value-based control hook.
    • Takes a value and returns a "deferred" copy to help delay re-rendering based on the new value.
    • Use it when you receive a prop or a value from another Hook and that value causes lag when rendering, but you do not manage where that value changes.
    • Does not have isPending like useTransition; you must compare manually (value !== deferredValue).


Create file UseTransition.tsx

import { LoadingOutlined, SearchOutlined } from "@ant-design/icons";
import { Card, Input, Space, Spin, Table, Typography } from "antd";
import { useState, useTransition } from "react";

const { Title, Text } = Typography;
const LIST = Array.from({ length: 10_000 }, (_, i) => i);

export function UseTransition() {
const [isPending, startTransition] = useTransition();
const [filterTerm, setFilterTerm] = useState("");
const [filteredList, setFilteredList] = useState<string[]>([]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterTerm(value);

startTransition(() => {
const filtered = LIST.map(i => `Result for "${value.toLowerCase()}" - Entry #${i + 1}`);
setFilteredList(filtered);
});
};

const columns = [
{
title: "Item Name",
dataIndex: "name",
key: "name",
},
];
const dataSource = filteredList.map((name, index) => ({ key: index, name }));

return (
<div className="min-h-screen bg-gray-50 p-8 flex justify-center">
<Card className="w-full max-w-2xl shadow-lg border-0">
<Space orientation="vertical" size="large" className="w-full">
<header className="border-b pb-4">
<Title level={2} className="!mb-1">
React Fiber
</Title>
<Text type="secondary">useTransition</Text>
</header>

<section>
<Input size="large" placeholder="Search items..." prefix={<SearchOutlined className="text-gray-400" />} value={filterTerm} onChange={handleChange} allowClear />
<div className="h-8 flex items-center mt-2">
{isPending && (
<Space className="text-blue-500">
<Spin indicator={<LoadingOutlined spin />} size="small" />
<Text className="text-blue-500 text-sm">Fiber is filtering data...</Text>
</Space>
)}
</div>
</section>

<section className="border border-gray-100 rounded-lg overflow-hidden">
<Table
dataSource={dataSource}
columns={columns}
pagination={false}
scroll={{ y: 400 }}
size="middle"
showHeader={false}
style={{ opacity: isPending ? 0.3 : 1, transition: "opacity 0.3s" }}
locale={{ emptyText: filterTerm ? "No results" : "Type to search" }}
/>
</section>
</Space>
</Card>
</div>
);
}

The code uses useTransition to separate two state updates. The setFilterTerm update happens immediately so users see the characters they just typed. Meanwhile, calculating the filter for a 10,000-item array and updating setFilteredList is wrapped in startTransition, marking it as a low-priority task. React will process it in the background and provide the isPending variable to show a Spin icon and a table blur effect, ensuring the UI never "freezes."


Create file UseDeferredValue.tsx

import { RocketOutlined, SearchOutlined } from "@ant-design/icons";
import type { TableColumnsType } from "antd";
import { Badge, Card, Input, Space, Table, Typography } from "antd";
import { memo, useDeferredValue, useMemo, useState } from "react";

const { Title, Text } = Typography;

const RAW_DATA = Array.from({ length: 10_000 }, (_, i) => ({
key: i,
title: `Entry #${i + 1}`,
}));

interface DataType {
key: number;
title: string;
content?: string;
}

const ListData = memo(({ query }: { query: string }) => {
const columns: TableColumnsType<DataType> = [
{
title: "Content Result",
dataIndex: "content",
render: (_, record) => (
<Text type="secondary">
Result for <span className="font-bold text-blue-500">"{query}"</span> - {record.title}
</Text>
),
},
];

const dataSource = useMemo(() => {
if (!query) return [];
return RAW_DATA.map(item => ({
...item,
content: query,
}));
}, [query]);

return (
<Table
className="border rounded-lg overflow-hidden"
columns={columns}
dataSource={dataSource}
pagination={false}
showHeader={false}
scroll={{ y: 400 }}
size="middle"
locale={{
emptyText: <div className="py-10 italic">Type to search...</div>,
}}
/>
);
});

export function UseDeferredValue() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);
const isStale = text !== deferredText;

return (
<div className="min-h-screen bg-slate-50 p-6 md:p-12 flex justify-center">
<Card className="w-full max-w-3xl shadow-xl rounded-xl border-0">
<Space orientation="vertical" size="large" className="w-full">
<header className="flex items-center gap-3 border-b pb-4">
<RocketOutlined className="text-blue-500 text-2xl" />
<div className="flex flex-col">
<Title level={3} className="!m-0 text-gray-800">
React Fiber
</Title>
<Text type="secondary">useDeferredValue</Text>
</div>
</header>

<section>
<div className="flex justify-between items-end mb-2">
<Text strong className="text-gray-600">
Search Input
</Text>
{isStale && <Badge status="processing" text={<Text className="text-blue-500 text-xs italic">Fiber is computing in background...</Text>} />}
</div>
<Input
size="large"
placeholder="Search items..."
prefix={<SearchOutlined className="text-gray-300" />}
value={text}
onChange={e => setText(e.target.value)}
className="rounded-lg shadow-sm border-gray-200"
allowClear
/>
</section>
<section
className="transition-opacity duration-200"
style={{
opacity: isStale ? 0.6 : 1,
filter: isStale ? "grayscale(20%)" : "none",
}}
>
<ListData query={deferredText} />
</section>
</Space>
</Card>
</div>
);
}

In this code, useDeferredValue is used to receive a delayed "copy" (deferredText) of the text value. When the user types quickly, the text variable is updated immediately so the Input remains smooth, while deferredText (used for heavy list filtering) will have its update deferred until the browser is idle. The isStale variable helps users recognize the UI is in an old data state by blurring the list, creating a more professional experience.

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Using Kafka with Docker and NodeJS

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Kubernetes Practice Series

Sitemap

DevOps Practice Series