# How Fre's reconcile() Function Performs Work with Time Slicing

> Discover how Fre's reconcile() function leverages time slicing. Learn how it breaks down tasks into chunks, yields to the browser, and resumes work efficiently for smoother UI performance.

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

---

**Fre's reconcile() function implements cooperative time-slicing by walking the fiber tree in small 5ms chunks, yielding control back to the browser when shouldYield() returns true, and resuming work later via a bound callback scheduled by the flush loop.**

Fre is a lightweight React-like UI library that prioritizes responsiveness during large component updates. The reconciliation engine located in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) coordinates with the scheduler in [`src/schedule.ts`](https://github.com/frejs/fre/blob/main/src/schedule.ts) to break rendering work into discrete time slices. This architecture ensures that lengthy tree traversals never block the main thread, allowing the browser to process user input and paint updates between computation chunks.

## The Cooperative Scheduling Architecture

### Initiating Updates and Task Scheduling

When a component triggers a state change, the `update` function marks the corresponding fiber as dirty and enqueues a reconciliation task. According to the Fre source code, this entry point in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) serves as the bridge between component updates and the time-sliced scheduler:

```typescript
// src/reconcile.ts
export const update = (fiber?: Fiber) => {
  if (!fiber.dirty) {
    fiber.dirty = true               // ← mark for work
    schedule(() => reconcile(fiber)) // ← enqueue a task
  }
}

```

The `schedule` function pushes the callback onto a global task queue and invokes `startTransition(flush)`, which begins the cooperative work loop defined in [`src/schedule.ts`](https://github.com/frejs/fre/blob/main/src/schedule.ts).

### The Flush Loop and Time Budget Management

The scheduler manages a shared time budget across all pending tasks. The `flush` function calculates a deadline based on a **5ms threshold** and processes jobs until that slice expires:

```typescript
// src/schedule.ts (flush)
deadline = getTime() + threshold      // 5 ms slice
let job = peek(queue)
while (job && !shouldYield()) {       // ← cooperative loop
  const { callback } = job
  job.callback = null
  const next = callback()             // ← call reconcile()
  // ...
}

```

The `shouldYield()` helper compares `performance.now()` against the calculated deadline. When the time slice expires, `flush` stops execution and returns control to the browser, preventing frame drops during heavy renders.

### Incremental Tree Traversal with Reconcile

The `reconcile` function itself respects the same deadline mechanism while walking the fiber tree. Implemented in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts), it processes one node at a time via the `capture` function, checking the time budget after each iteration:

```typescript
// src/reconcile.ts
const reconcile = (fiber?: Fiber) => {
  while (fiber && !shouldYield()) {   // ← respects the same deadline
    fiber = capture(fiber) as any
  }
  return fiber ? reconcile.bind(null, fiber) : null
}

```

**Key mechanisms of this loop:**

- **Single-node processing**: Each `capture(fiber)` call performs the reconciliation work for one specific fiber node.
- **Cooperative yielding**: After every node, `shouldYield()` checks if the 5ms budget is exhausted.
- **Continuation callbacks**: If work remains (fiber is not null), `reconcile` returns a bound function (`reconcile.bind(null, fiber)`) that the scheduler will invoke in the next time slice.

When the current slice ends, the scheduler reinstalls a new callback via `task(shouldYield())` to invoke this bound function, effectively pausing and resuming the tree walk without losing state.

### Commit Phase Integration

Inside the `capture` function's sibling traversal logic, Fre checks `fiber.dirty` to determine when a component has produced new output. When detected, the `commit` function (from [`src/commit.ts`](https://github.com/frejs/fre/blob/main/src/commit.ts)) applies DOM mutations immediately after the fiber finishes its work. The scheduler can yield immediately following a commit, keeping UI updates incremental and non-blocking.

## Practical Code Examples

### Small Component Trees (Single Slice)

For tiny component trees, the reconciliation completes within the initial 5ms budget because `shouldYield()` never becomes true:

```typescript
import { render } from './reconcile'
import { h } from './h'          // create JSX-like elements

function App() {
  return h('div', null, 'Hello Fre')
}

// Initial mount – render schedules reconcile which finishes in one slice
render(h(App), document.getElementById('root')!)

```

When the tree contains few fibers, the while-loop in `reconcile` processes all nodes before the deadline, completing the render in a single frame.

### Large Lists with Time-Slicing

For substantial component trees, such as lists with thousands of items, Fre automatically splits the work across multiple frames:

```typescript
import { render, update } from './reconcile'
import { h } from './h'

function LargeList({ count }: { count: number }) {
  const items = []
  for (let i = 0; i < count; i++) {
    items.push(h('li', { key: i }, `Item ${i}`))
  }
  return h('ul', null, items)
}

// Mount a list of 10,000 items
render(h(LargeList, { count: 10000 }), document.body)

// Later, trigger an update that changes many items
update(/* reference to the root fiber stored by render */)

```

With 10,000 fibers to process, `reconcile` repeatedly encounters `shouldYield()` returning true. The scheduler yields after each 5ms slice, allowing the browser to paint and respond to input before continuing the walk in the next frame.

### Custom Scheduled Work

Any function scheduled through the scheduler automatically inherits the same time-slicing behavior:

```typescript
import { schedule, shouldYield } from './schedule'

// Custom heavy computation respecting Fre's time slice
schedule(() => {
  while (!shouldYield()) {
    // Process large dataset chunk here
    processNextBatch()
  }
})

```

This pattern leverages the same `flush` loop and `shouldYield` gate used by the reconciliation engine, ensuring custom logic cooperates with Fre's rendering priorities.

## Key Source Files

- **[`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts)**: Contains the `reconcile()` and `update()` functions, fiber traversal logic, and integration with the scheduler.
- **[`src/schedule.ts`](https://github.com/frejs/fre/blob/main/src/schedule.ts)**: Implements the cooperative scheduler with `schedule()`, `flush()`, and the `shouldYield()` deadline checking.
- **[`src/commit.ts`](https://github.com/frejs/fre/blob/main/src/commit.ts)**: Handles the DOM mutation phase triggered after fiber work completes.
- **[`src/type.ts`](https://github.com/frejs/fre/blob/main/src/type.ts)**: Defines TypeScript interfaces for `Fiber`, `Task`, and scheduling constants.
- **[`src/h.ts`](https://github.com/frejs/fre/blob/main/src/h.ts)**: Provides the `h()` helper for creating virtual node structures consumed by reconciliation.

## Summary

- **Fre implements a 5ms time slice** (threshold) for all reconciliation work, enforced by the `shouldYield()` function in [`src/schedule.ts`](https://github.com/frejs/fre/blob/main/src/schedule.ts).
- **The `reconcile()` function** processes one fiber at a time via `capture()`, checking the time budget after each node to maintain responsiveness.
- **Incomplete work survives across frames** through bound continuation callbacks that the scheduler re-invokes in subsequent time slices.
- **Updates start with `update()`**, which marks fibers dirty and enqueues them via `schedule()`, initiating the `flush` work loop.
- **DOM commits occur immediately** after a fiber completes, but the scheduler can yield right after, keeping UI updates incremental.

## Frequently Asked Questions

### What is the default time slice duration in Fre?

The default threshold is **5 milliseconds**, defined as a constant in the scheduler (`threshold = 5`). This value represents the maximum time the `flush` loop and `reconcile` function will execute before yielding control back to the browser via `shouldYield()`.

### How does Fre decide when to pause reconciliation?

Fre uses the `shouldYield()` function located in [`src/schedule.ts`](https://github.com/frejs/fre/blob/main/src/schedule.ts) to check if `performance.now()` has exceeded the deadline calculated at the start of each slice (`getTime() + threshold`). Both the scheduler's `flush` loop and the `reconcile` function consult this gate after processing each unit of work.

### What happens to unfinished reconciliation work?

When `reconcile` encounters the deadline mid-traversal, it returns a bound function (`reconcile.bind(null, fiber)`) containing the next fiber to process. The scheduler captures this return value and re-schedules it as a new task, allowing the tree walk to resume exactly where it paused in the next available time slice.

### Is the DOM commit phase also time-sliced?

No, the commit phase is synchronous. When `capture` detects that a fiber has new output (via the `dirty` flag), it immediately invokes `commit()` from [`src/commit.ts`](https://github.com/frejs/fre/blob/main/src/commit.ts) to apply DOM mutations. However, the scheduler yields immediately after the commit completes, ensuring that even large commit batches do not arbitrarily extend beyond the time slice if multiple fibers finish within the same budget.