How React Fiber Eliminates UI Jank: A Step‑by‑Step Deep Dive

This article explains why web pages feel sluggish, how React Fiber’s interruptible asynchronous rendering solves the jank problem, and walks through progressively refined implementations—from a basic React demo to a full Fiber renderer with work‑loop scheduling, diffing, and performance optimizations.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How React Fiber Eliminates UI Jank: A Step‑by‑Step Deep Dive

Why UI Jank Happens

Jank is the perception of choppy rendering. Human visual perception is most comfortable at 50‑60 Hz, which corresponds to a 60 fps baseline. When the browser’s render interval exceeds the ~16.6 ms needed for one 60 Hz frame, the animation appears disjointed.

Human perception : the eye detects motion best between 50 Hz and 60 Hz.

Frame rate vs. smoothness : higher fps yields more static images per second, creating smoother motion.

Render interval : intervals > 16.6 ms cause visible jitter.

Browser Rendering Cycle

The browser’s event loop does not render on every tick; rendering occurs only when there is idle time. Key questions include:

Does every event‑loop tick trigger a render?

When does requestAnimationFrame run—before or after painting, and relative to micro‑tasks?

When does requestIdleCallback run, and can it be forced with a timeout?

When are resize and scroll events dispatched?

Rendering is not guaranteed each loop; it occurs when there are pending micro‑tasks. requestAnimationFrame runs before the browser paints, making it ideal for animation work. requestIdleCallback runs after painting and may be delayed unless a timeout is supplied. resize and scroll are throttled and dispatched during the render phase of the event loop.

Fiber diagram
Fiber diagram

How Fiber Solves Jank

React Fiber introduces interruptible asynchronous rendering. The synchronous, non‑interruptible update process is split into small work units that can be paused, allowing the browser to handle higher‑priority tasks and then resume rendering.

Native React Example

import React from 'react';
import ReactDOM from 'react-dom';
const container = document.querySelector("#root");
const element = React.createElement('div', {title: 'div', name: 'div'},
  'div ',
  React.createElement('h1', null, 'h1'),
  React.createElement('p', null, 'p'),
  React.createElement('h2', null, 'h2')
);
ReactDOM.render(element, container);
Native React render result
Native React render result

Implementing createElement and render

createElement.js

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children?.map(child => typeof child === 'object' ? child : createTextElement(child))
    }
  };
}
function createTextElement(text) {
  return { type: 'TEXT_ELEMENT', props: { nodeValue: text, children: [] } };
}
export default { createElement };

render.js (first version)

function render(element, container) {
  const dom = element.type === 'TEXT_ELEMENT'
    ? document.createTextNode('')
    : document.createElement(element.type);
  const isProperty = key => key !== 'children';
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => dom[name] = element.props[name]);
  element.props.children?.forEach(child => render(child, dom));
  container.appendChild(dom);
}
export default { render };

Problem: this naïve implementation cannot be interrupted; long JavaScript execution blocks rendering and causes jank.

First Optimization – Work Loop with requestIdleCallback

Introduce a global nextUnitOfWork and schedule work during idle periods, allowing the browser to pause and resume rendering.

let nextUnitOfWork = null;
function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
  if (!fiber.dom) fiber.dom = createDOM(fiber);
  if (fiber.parent) fiber.parent.dom.appendChild(fiber.dom);
  const elements = fiber.props.children;
  let prevSibling = null;
  elements.forEach((child, i) => {
    const newFiber = { parent: fiber, props: child.props, type: child.type, dom: null, sibling: null };
    if (i === 0) fiber.child = newFiber; else prevSibling.sibling = newFiber;
    prevSibling = newFiber;
  });
  if (fiber.child) return fiber.child;
  let next = fiber;
  while (next) {
    if (next.sibling) return next.sibling;
    next = next.parent;
  }
  return null;
}

Issue: each fiber is still appended to the real DOM immediately, causing frequent re‑flows and a progressive “building‑up” visual effect.

Second Optimization – Batch DOM Commit

Collect all DOM nodes first, then commit them in one batch after the work loop finishes.

function workLoop(deadline) {
  while (nextUnitOfWork && !shouldYield) { /* same as before */ }
  if (!nextUnitOfWork && wipRoot) commitRoot();
  requestIdleCallback(workLoop);
}
function commitRoot() {
  commitWork(wipRoot.child);
  wipRoot = null;
}
function commitWork(fiber) {
  if (!fiber) return;
  const parentDom = fiber.parent.dom;
  parentDom.appendChild(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

Remaining problem: the whole tree is still re‑rendered on every update, leading to high overhead.

Third Optimization – Diff Algorithm

Introduce a reconciliation step that compares the new fiber tree with the previous one, generating PLACEMENT, UPDATE, and DELETION effect tags so that only changed parts are committed.

function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;
  while (index < elements.length || oldFiber) {
    const element = elements[index];
    const sameType = oldFiber && element && oldFiber.type === element.type;
    let newFiber = null;
    if (sameType) {
      newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: 'UPDATE' };
    } else if (element && !sameType) {
      newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: 'PLACEMENT' };
    } else if (oldFiber && !sameType) {
      oldFiber.effectTag = 'DELETION';
      deletions.push(oldFiber);
    }
    if (oldFiber) oldFiber = oldFiber.sibling;
    if (index === 0) wipFiber.child = newFiber; else if (newFiber) prevSibling.sibling = newFiber;
    prevSibling = newFiber;
    index++;
  }
}
function commitRoot() {
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}
Fiber diff diagram
Fiber diff diagram

Conclusion

Fiber’s core contribution is an interruptible, priority‑driven rendering pipeline that prevents long JavaScript tasks from blocking the UI. Real implementations also include sophisticated task scheduling, fine‑grained lifecycle control, and a diff algorithm to minimize DOM mutations.

Further exploration can cover priority queues, concurrent mode, and advanced hooks.

frontendPerformanceJavaScriptReActVirtual DOMFiberDiff Algorithm
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.