How Fre's reconcile() Function Performs Work with Time Slicing
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 coordinates with the scheduler in 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 serves as the bridge between component updates and the time-sliced scheduler:
// 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.
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:
// 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, it processes one node at a time via the capture function, checking the time budget after each iteration:
// 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),
reconcilereturns 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) 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:
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:
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:
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: Contains thereconcile()andupdate()functions, fiber traversal logic, and integration with the scheduler.src/schedule.ts: Implements the cooperative scheduler withschedule(),flush(), and theshouldYield()deadline checking.src/commit.ts: Handles the DOM mutation phase triggered after fiber work completes.src/type.ts: Defines TypeScript interfaces forFiber,Task, and scheduling constants.src/h.ts: Provides theh()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 insrc/schedule.ts. - The
reconcile()function processes one fiber at a time viacapture(), 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 viaschedule(), initiating theflushwork 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 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 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.
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 →