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 decisionsdirty– a boolean flag indicating the fiber needs re-rendering- Tree pointers –
parent,sibling,child, andalternatelinks 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 slicedeadline– Calculated asnow + threshold, representing the cutoff time for current workshouldYield()– A function (lines 49-51) that returnstruewhen 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:
-
Trigger –
setStateordispatchmarks a fiber as dirty (fiber.dirty = true) and callsupdate(fiber) -
Schedule –
updatepushes a reconciliation task onto the scheduler viaschedule(() => reconcile(fiber)) -
Time-slice execution – The
flushloop begins processing. It runs the reconcile callback untilshouldYield()indicates the 5ms threshold is reached -
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 -
Resume – The scheduler yields to the browser. When the next frame is available,
flushresumes the reconcile loop from the saved continuation -
Commit – When reconciliation completes for a fiber,
commitimmediately 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.tscarrylane,dirtyflags, and tree pointers (parent,sibling,child,alternate) that enable incremental, resumable tree traversal - Cooperative scheduling in
src/schedule.tsuses a 5msthreshold,deadlinetracking, andshouldYield()to pause work without blocking the main thread - Incremental reconciliation in
src/reconcile.tschecksshouldYield()after each fiber via awhile (!shouldYield())loop, returning continuations when time expires - Atomic commits in
src/commit.tsapply DOM changes synchronously after reconciliation completes, ensuring visual consistency startTransitionAPI 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →