How Fre's Fiber Architecture Enables Concurrent Rendering with Time Slicing

Fre implements a cooperative scheduling system that breaks rendering work into tiny Fiber units and processes them in bounded time slices, allowing the browser to remain responsive during heavy updates.

Fre is a lightweight, React-like library that achieves concurrent rendering through a custom Fiber architecture. Unlike traditional virtual DOM implementations that process updates in a single blocking pass, Fre's rendering engine incrementally walks the component tree and yields control back to the browser when time expires. This article examines the source code to explain how Fre's Fiber nodes, scheduler, and reconciliation loop work together to enable time-sliced rendering without blocking the main thread.

Fiber Architecture: Incremental Work Units

Fre's concurrent capabilities begin with the Fiber data structure defined in src/type.ts. Each node in the virtual tree carries metadata that allows the renderer to treat rendering as resumable work rather than an atomic operation.

The Fiber interface (lines 62-99) includes:

  • lane – priority metadata for scheduling decisions
  • dirty – a boolean flag indicating the fiber needs re-rendering
  • Tree pointersparent, sibling, child, and alternate links that create a traversable graph structure

These fields enable the reconciler to walk the tree incrementally. Because each fiber maintains its own state and relationships, the renderer can pause mid-traversal, save its position, and resume later without losing context. The alternate field specifically links to the previous render's fiber, enabling comparison between old and new trees during reconciliation.

Cooperative Scheduling and Time Slicing

The scheduling mechanism lives in src/schedule.ts and implements a cooperative task queue that respects time budgets. This system ensures that rendering never monopolizes the main thread.

At the core of the scheduler are three critical components:

  • threshold – A constant (defaulting to 5ms) defining the maximum duration of a single time slice
  • deadline – Calculated as now + threshold, representing the cutoff time for current work
  • shouldYield() – A function (lines 49-51) that returns true when the current time exceeds the deadline

The flush function (lines 32-46) processes the task queue by executing callbacks until shouldYield() reports that the slice is exhausted. If work remains unfinished, the scheduler re-queues itself via startTransition, allowing the browser to process input, animation, or network events before the next slice begins.

For low-priority updates, Fre exposes startTransition (lines 8-10), which queues callbacks in a transitions array and immediately invokes the scheduler, ensuring that urgent updates (like text input) can interrupt background rendering.

The Concurrent Reconciliation Loop

While the scheduler manages when to run work, src/reconcile.ts determines how to process that work incrementally. The reconcile function implements the render phase of Fre's Fiber architecture, walking the tree via a capture → sibling → bubble pattern.

The key concurrency mechanism appears in the reconcile loop (lines 39-44):

while (!shouldYield()) {
  // Process next fiber: capture, sibling, or bubble
  capture(fiber)
}

After processing each fiber, the loop checks shouldYield(). If the deadline has passed, reconcile returns a bound function that captures the current fiber state. The scheduler stores this continuation and resumes execution in the next time slice. This allows Fre to pause rendering mid-tree—potentially after processing thousands of components—and resume exactly where it left off without recalculating completed work.

The Fast Commit Phase

Once the reconciler completes a fiber's work, the actual DOM mutation occurs in src/commit.ts. The commit phase is intentionally kept minimal and synchronous because it touches the DOM, which must remain consistent to avoid visual tearing.

The commit functions (lines 5-44) perform operations like inserting, updating, and removing DOM nodes. Because the heavy lifting of calculating changes happens during the interruptible reconcile phase, the commit phase executes quickly. This separation of concerns—calculating in time-sliced chunks and committing atomically—ensures that users see consistent UI states while still benefiting from non-blocking rendering.

How Concurrent Rendering Works: End-to-End

Understanding how Fre's Fiber architecture enables concurrent rendering requires seeing how the pieces integrate during a typical state update:

  1. TriggersetState or dispatch marks a fiber as dirty (fiber.dirty = true) and calls update(fiber)

  2. Scheduleupdate pushes a reconciliation task onto the scheduler via schedule(() => reconcile(fiber))

  3. Time-slice execution – The flush loop begins processing. It runs the reconcile callback until shouldYield() indicates the 5ms threshold is reached

  4. Incremental render – Inside reconcile, Fre walks the fiber tree. After each fiber processed, it checks the deadline. If time expires, it returns a continuation to the scheduler

  5. Resume – The scheduler yields to the browser. When the next frame is available, flush resumes the reconcile loop from the saved continuation

  6. Commit – When reconciliation completes for a fiber, commit immediately applies DOM changes. The next slice then processes the next pending fiber

This pipeline ensures that even during complex updates with thousands of components, the browser can process user input every 5ms, delivering the smooth, responsive experience characteristic of concurrent rendering.

Practical Examples of Concurrent Rendering

Basic State Updates with Automatic Time Slicing

Standard state updates automatically benefit from Fre's concurrent rendering without requiring explicit API calls:

import { render, useState } from 'fre';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Clicked {count} times
    </button>
  );
}

render(<Counter />, document.getElementById('root')!);

When setCount executes, Fre marks the Counter fiber as dirty and schedules reconciliation. The scheduler processes this update in 5ms slices, checking shouldYield after each fiber in the subtree. Even if the component tree were deeply nested, user interactions could interrupt the rendering to keep the UI responsive.

Deferring Heavy Computation with startTransition

For CPU-intensive updates that should not block high-priority interactions, use startTransition:

import { render, startTransition, useState } from 'fre';

function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);

  const handleChange = (e: Event) => {
    const value = (e.target as HTMLInputElement).value;
    setQuery(value);

    // Defer expensive filtering to a transition (low priority)
    startTransition(() => {
      const filtered = heavySearch(value); // CPU-heavy operation
      setResults(filtered);
    });
  };

  return (
    <>
      <input value={query} onInput={handleChange} />
      <ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
    </>
  );
}

render(<Search />, document.getElementById('app')!);

Here, startTransition (defined in src/schedule.ts) queues the heavy search operation as a low-priority task. If the user types rapidly, the urgent text input updates (high priority) can interrupt and delay the expensive filtering (low priority), preventing input lag.

Adjusting the Time Slice Threshold

You can modify the scheduler's aggressiveness by changing the slice duration in src/schedule.ts:

const threshold: number = 8; // Increase to 8ms per slice

Increasing the threshold reduces context switches and may improve throughput on fast devices, while decreasing it (to 2-3ms) improves responsiveness on slower devices at the cost of more scheduler overhead.

Summary

  • Fiber nodes in src/type.ts carry lane, dirty flags, and tree pointers (parent, sibling, child, alternate) that enable incremental, resumable tree traversal
  • Cooperative scheduling in src/schedule.ts uses a 5ms threshold, deadline tracking, and shouldYield() to pause work without blocking the main thread
  • Incremental reconciliation in src/reconcile.ts checks shouldYield() after each fiber via a while (!shouldYield()) loop, returning continuations when time expires
  • Atomic commits in src/commit.ts apply DOM changes synchronously after reconciliation completes, ensuring visual consistency
  • startTransition API allows developers to explicitly mark updates as low-priority, enabling urgent interactions to interrupt background rendering

Frequently Asked Questions

What is the default time slice threshold in Fre?

Fre uses a default threshold of 5 milliseconds defined in src/schedule.ts. This value represents the maximum duration the scheduler will process work before yielding control back to the browser, ensuring that user input and animations remain smooth even during heavy rendering loads.

How does Fre decide when to yield the main thread?

The scheduler calculates a deadline (current time plus threshold) when beginning a slice. After processing each fiber, the reconcile function in src/reconcile.ts checks shouldYield(), which compares the current time against this deadline. If the deadline has passed, the reconciler immediately returns a continuation function to resume work later, allowing the browser to process other events.

What is the difference between the reconcile phase and commit phase?

The reconcile phase (in src/reconcile.ts) is interruptible and time-sliced—it calculates the differences between fiber trees and can pause mid-execution. The commit phase (in src/commit.ts) is synchronous and atomic—it applies the calculated DOM changes in a single pass to ensure the UI remains consistent. The heavy work happens in the interruptible reconcile phase, while the commit phase remains fast.

Can I adjust the time slicing threshold for different devices?

Yes. You can modify the threshold constant in src/schedule.ts to tune the trade-off between rendering throughput and responsiveness. Increase the value (e.g., to 8-10ms) for powerful devices to reduce scheduling overhead, or decrease it (e.g., to 2-3ms) for low-end devices to prioritize input responsiveness over raw rendering speed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →