# How Fre's `sibling()` Function Traverses Sibling Fibers in the Reconciliation Loop

> Discover how Fre's sibling() function traverses sibling fibers using post-order depth-first traversal to commit dirty fibers and find the next work unit.

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

---

**Fre's `sibling()` function performs a post-order depth-first traversal by bubbling side-effects, committing dirty fibers, and climbing the tree to find the next work unit until the root is reached.**

Fre is a lightweight, React-like library that manages UI updates through a **Fiber** architecture. At the heart of its rendering engine lies the `sibling()` function in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts), which determines how the reconciler moves between nodes after processing a fiber's children. Understanding this traversal mechanism is essential for grasping how Fre implements cooperative scheduling and incremental rendering.

## Where `sibling()` Fits in the Reconciliation Pipeline

The reconciliation process begins in the `reconcile()` function, which repeatedly calls `capture()` to process fibers until the scheduler's `shouldYield()` indicates a pause is needed. When `capture()` finishes processing a fiber's children, it invokes `sibling()` to locate the next unit of work:

```typescript
// In src/reconcile.ts - capture() logic
if (fiber.isComp) {
  // Component-specific reconciliation
} else {
  updateHost(fiber as FiberHost);
}
return fiber.child || sibling(fiber);  // Child first, then sibling traversal

```

The outer reconciliation loop drives the process:

```typescript
while (fiber && !shouldYield()) {
  fiber = capture(fiber) as any;
}

```

When `capture()` returns `null` (signaling the frame is complete), the scheduler yields control back to the browser.

## Step-by-Step Traversal Logic in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts)

The `sibling()` implementation (located at lines 18-30 in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts)) follows a strict post-order traversal pattern that processes children before moving sideways or upward.

### 1. Bubbling Side Effects

Upon entry, `sibling()` immediately calls `bubble(fiber)` to execute any layout effects or cleanup callbacks associated with the completed fiber. This ensures component lifecycles are processed in the correct order before moving to the next node.

### 2. Committing Dirty Fibers

The function checks the `fiber.dirty` flag to determine if the fiber requires DOM updates:

- If `fiber.dirty` is set, the flag is cleared and `commit(fiber)` is invoked to apply changes to the real DOM
- The function returns **`null`** immediately after committing, signaling the current frame should end
- This pause allows the scheduler to batch DOM writes and maintain frame budget constraints

### 3. Moving to the Next Sibling

If the fiber is not dirty, `sibling()` checks for `fiber.sibling`. When a sibling reference exists, that fiber is returned immediately, allowing the reconciler to process the next node at the same tree depth.

### 4. Climbing the Tree

When no sibling exists, the algorithm enters a climbing loop:

- It sets `fiber = fiber.parent` and repeats the bubble-check-commit cycle
- This continues until either a sibling is found at a higher level or the root is reached
- If the root is reached without finding siblings, `null` is returned, indicating the entire tree has been traversed

## Post-Order Depth-First Traversal Pattern

The traversal strategy implemented by `sibling()` is a **post-order depth-first search** that moves right-to-left across the Fiber tree:

- **Down**: `capture()` descends into `fiber.child` first, processing entire subtrees before moving sideways
- **Sideways**: `sibling()` moves to the next sibling only after the current branch is complete
- **Up**: When siblings are exhausted, the algorithm climbs to parents, ensuring parent effects run after all children are processed

This pattern guarantees that component cleanup and layout effects execute in the correct hierarchical order, and that DOM commits happen only after a fiber and its entire subtree have been reconciled.

## Practical Example: Component Tree Walking

Consider a component structure where `sibling()` manages the traversal flow:

```tsx
import { render, useState, useEffect } from 'fre'

function Parent() {
  return (
    <div>
      <ChildA />
      <ChildB />
    </div>
  )
}

function ChildA() {
  useEffect(() => console.log('ChildA committed'), [])
  return <span>A</span>
}

function ChildB() {
  useEffect(() => console.log('ChildB committed'), [])
  return <span>B</span>
}

render(<Parent />, document.getElementById('root'))

```

**Traversal sequence:**
1. `capture()` enters `Parent`, then `ChildA`
2. After reconciling `ChildA`, `sibling()` finds `ChildB` and returns it
3. After `ChildB` finishes, `sibling()` climbs to `Parent`, finds no sibling, and continues upward
4. At the root, `sibling()` returns `null`, ending the frame

The `bubble()` calls ensure `ChildA`'s effect runs before `ChildB`'s, and both run before the parent completes.

## Summary

- **`sibling()`** in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) manages post-order traversal by processing side-effects, committing dirty fibers, and climbing the tree when siblings are exhausted
- The function returns `null` either when committing a dirty fiber (to yield to the scheduler) or when reaching the root (indicating traversal completion)
- **Post-order depth-first** traversal ensures children are fully processed before parents, maintaining correct effect ordering
- The combination of `capture()` (downward) and `sibling()` (sideways/upward) creates Fre's cooperative scheduling system that respects browser frame budgets

## Frequently Asked Questions

### What is a Fiber in Fre?

A **Fiber** is a lightweight JavaScript object representing a unit of work in Fre's rendering pipeline. Each Fiber corresponds to a React element or DOM node and contains references to its `child`, `sibling`, and `parent`, along with state, props, and effect flags like `dirty`.

### How does `sibling()` know when to stop traversing?

`sibling()` stops when it reaches the root of the Fiber tree without finding any remaining siblings. At that point, it returns `null`, which signals to the `reconcile()` loop that the entire tree has been processed and the current frame is complete.

### What is the difference between `capture()` and `sibling()` in Fre?

**`capture()`** handles the downward phase of reconciliation, entering into child fibers and performing component updates or host node creation. **`sibling()`** handles the upward and sideways phase, moving to next siblings after children are complete or climbing to parents when a branch is finished.

### When does `sibling()` trigger a DOM commit?

`sibling()` triggers a DOM commit via `commit(fiber)` whenever the current fiber has the `dirty` flag set to `true`. This typically happens after state updates or initial renders. After committing, `sibling()` returns `null` to allow the scheduler to pause work and maintain performance.