Performance Optimization in React: Diff Computation, Fiber Architecture, and Custom Renderers
The article explains how React’s virtual‑DOM diff can become a performance bottleneck, reviews classic optimizations such as debouncing, PureComponent, and idle callbacks, then details how the Fiber architecture makes diffing interruptible and prioritized and how the same reconciler can be leveraged to build custom cross‑platform renderers.
React uses a Virtual DOM and a diff algorithm to update the real DOM. While this simplifies development, the diff computation is not free and can become a performance bottleneck when many elements are involved, leading to dropped frames and sluggish UI.
The main reasons are:
Older React versions (<15) use a Stack Reconciler that performs recursive diffing, which cannot be interrupted.
The browser runs JavaScript and rendering on separate threads that are mutually exclusive; heavy JavaScript execution blocks UI rendering.
Common front‑end performance optimizations include:
Debounce
Delay state updates until user input is finished, preventing frequent re‑renders.
class App extends Component {
onChange = () => {
if (this.timeout) { clearTimeout(this.timeout); }
this.timeout = setTimeout(() => {
this.setState({ ds: [] });
}, 200);
};
render() {
return (
);
}
}PureComponent / shouldComponentUpdate
Use shallow comparison of props and state to skip unnecessary diff calculations.
class App extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
!shallowEqual(nextProps, this.props) ||
!shallowEqual(nextState, this.state)
);
}
render() { /* same as above */ }
}Key considerations when using this approach:
a. Only shallow comparison is feasible; deep comparison may be slower than the diff itself.
b. Preserve object reference integrity when updating state; mutating nested objects can prevent updates.
class App extends PureComponent {
state = { record: {} };
componentDidMount() {
const { record } = this.state;
record.name = "demo";
this.setState({ record });
}
render() { return <>{this.state.record.name}; }
}c. Functions that capture external variables must be aware of changes to those variables.
class App extends PureComponent {
cellRender = (value, index, record) => {
return record.name + this.name;
};
render() { return
; }
}Object Hijacking (MobX / Vue style)
Observe objects directly and avoid calling setState, enabling fine‑grained updates.
@inject("color")
@observer
class Btn extends React.Component {
render() {
return (
{this.props.text}
);
}
}Only the button re‑renders when its color changes.
requestIdleCallback
The browser API allows scripts to run during idle periods, which can be used to defer low‑priority work such as diff calculations.
requestIdleCallback(deadline => {
if (deadline.timeRemaining() > 0) {
// perform work
} else {
requestIdleCallback(otherTasks);
}
});Using idle time raises challenges: the diff must be interruptible and resumable, and tasks need priority tagging. React Fiber was introduced to address these concerns.
Fiber‑based Fibonacci Example
Fiber transforms recursive algorithms into an explicit loop with a stack‑like structure, preserving intermediate state for interruption.
function fib(n) {
let fiber = { arg: n, returnAddr: null, a: 0 };
rec: while (true) {
if (fiber.arg <= 2) {
let sum = 1;
while (fiber.returnAddr) {
fiber = fiber.returnAddr;
if (fiber.a === 0) {
fiber.a = sum;
fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
continue rec;
}
sum += fiber.a;
}
return sum;
} else {
fiber = { arg: fiber.arg - 1, returnAddr: fiber, a: 0 };
}
}
}React Fiber adopts a similar approach, expanding the call stack into a linked list of Fiber nodes.
Typical Fiber node fields include tag, type, key, child, sibling, return, pendingProps, memoizedProps, pendingWorkPriority, stateNode, effectTag, etc.
{
tag,
type,
key,
child,
sibling,
return,
pendingProps,
memoizedProps,
pendingWorkPriority,
stateNode,
effectTag,
...
}Custom Renderer
React Reconciler can be used to build a custom renderer by implementing a HostConfig. The core method createInstance maps a component type to a platform‑specific instance (e.g., document.createElement for the web).
import Reconciler from 'react-reconciler';
const HostConfig = { /* ... */ };
const CustomRenderer = Reconciler(HostConfig);
let root;
function render(children, container) {
if (!root) {
root = CustomRenderer.createContainer(container);
}
CustomRenderer.updateContainer(children, root);
}
render(
, document.querySelector('#root'));Cross‑platform renderers can intercept createInstance to return native components, such as a MobileButton for mobile targets.
import { MobileButton } from 'xxx';
createInstance(type, props) {
const components = { Button: MobileButton };
return new components[type](props);
}API design trade‑offs include special handling for text nodes (shouldSetTextContent, createTextInstance) that are necessary for the DOM but often unnecessary for other hosts.
export function shouldSetTextContent(type, props) {
return (
type === 'textarea' ||
type === 'option' ||
type === 'noscript' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
props.dangerouslySetInnerHTML.__html != null)
);
}In non‑DOM environments the function can simply return false.
export function shouldSetTextContent() { return false; }Conclusion
The article explores why React introduced Fiber, how its architecture enables interruptible and prioritized rendering, and how the Custom Renderer pattern leverages the same reconciler for diverse platforms. It also provides code snippets and references for further study.
NetEase Cloud Music Tech Team
Official account of NetEase Cloud Music Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.