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

> Learn how Fre's Fiber architecture uses time slicing to break rendering into small units, keeping your browser responsive even during complex updates.

- Repository: [frejs/fre](https://github.com/frejs/fre)
- Tags: internals
- Published: 2026-03-02

---

**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`](https://github.com/frejs/fre/blob/main/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 pointers** – `parent`, `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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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):

```javascript
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`](https://github.com/frejs/fre/blob/main/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. **Trigger** – `setState` or `dispatch` marks a fiber as dirty (`fiber.dirty = true`) and calls `update(fiber)`

2. **Schedule** – `update` 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:

```tsx
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`:

```tsx
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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/src/schedule.ts):

```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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) checks `shouldYield()` after each fiber via a `while (!shouldYield())` loop, returning continuations when time expires
- **Atomic commits** in [`src/commit.ts`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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.