|
| 1 | +# Fiber Architecture Overview |
| 2 | + |
| 3 | +## Introduction |
| 4 | + |
| 5 | +The fiber architecture is the backbone of MiniReact's rendering system. If you're familiar with React, this should feel pretty similar. The core idea is breaking down rendering work into small units that can be paused, resumed, and prioritized. |
| 6 | + |
| 7 | +## What is a Fiber? |
| 8 | + |
| 9 | +A fiber is basically a JavaScript object that represents a unit of work. Think of it as a node in a tree where each node knows about its parent, children, and siblings. This structure lets us traverse the tree without recursion, which is key for being able to pause and resume work. |
| 10 | + |
| 11 | +```typescript |
| 12 | +interface Fiber { |
| 13 | + // Identity - what kind of thing is this? |
| 14 | + type: ElementType // 'div', a function, or a symbol |
| 15 | + key: string | null // For reconciliation |
| 16 | + |
| 17 | + // Relationships - how fibers connect to each other |
| 18 | + return: Fiber | null // Parent fiber |
| 19 | + child: Fiber | null // First child |
| 20 | + sibling: Fiber | null // Next sibling |
| 21 | + index: number // Position among siblings |
| 22 | + |
| 23 | + // State - what's the current status? |
| 24 | + stateNode: Node | ... // The actual DOM node or metadata |
| 25 | + pendingProps: Props // Props we're working with |
| 26 | + memoizedProps: Props // Props from last commit |
| 27 | + |
| 28 | + // Work tracking |
| 29 | + alternate: Fiber | null // Link to the other tree |
| 30 | + effectTag: EffectTag // What needs doing (place, update, delete) |
| 31 | + |
| 32 | + // Effects - side effects to apply |
| 33 | + nextEffect: Fiber | null |
| 34 | + firstEffect: Fiber | null |
| 35 | + lastEffect: Fiber | null |
| 36 | + deletions: Fiber[] | null |
| 37 | + |
| 38 | + // Hooks and context |
| 39 | + hooks: Hook[] | null |
| 40 | + hookCursor: number |
| 41 | + contextValues: Map<Context, unknown> | null |
| 42 | + ref: RefObject | RefCallback | null |
| 43 | +} |
| 44 | +``` |
| 45 | + |
| 46 | +## The Double-Buffer Pattern |
| 47 | + |
| 48 | +Here's where it gets interesting. We maintain two fiber trees at any given time: |
| 49 | + |
| 50 | +**Current Tree**: This reflects what's actually in the DOM right now. It's the committed state that users can see. |
| 51 | + |
| 52 | +**Work-in-Progress Tree**: This is what we're building during the render phase. It's like a draft that we can throw away if something goes wrong. |
| 53 | + |
| 54 | +These trees are linked through the `alternate` property. When we start rendering, we clone the current tree to create the work-in-progress tree. After we finish rendering and commit the changes, the work-in-progress becomes the new current. |
| 55 | + |
| 56 | +``` |
| 57 | +Current Tree Work-in-Progress Tree |
| 58 | +┌─────────┐ ┌─────────┐ |
| 59 | +│ Root │ <─alternate─>│ Root │ |
| 60 | +└────┬────┘ └────┬────┘ |
| 61 | + │ │ |
| 62 | +┌────▼────┐ ┌────▼────┐ |
| 63 | +│ App │ <─alternate─>│ App │ |
| 64 | +└────┬────┘ └────┬────┘ |
| 65 | + │ │ |
| 66 | +┌────▼────┐ ┌────▼────┐ |
| 67 | +│ Div │ <─alternate─>│ Div │ |
| 68 | +└─────────┘ └─────────┘ |
| 69 | +``` |
| 70 | + |
| 71 | +Why bother with two trees? A few reasons: |
| 72 | + |
| 73 | +1. **Error Recovery**: If something crashes during render, we still have the current tree intact |
| 74 | +2. **Concurrent Mode Ready**: We can work on updates without disrupting what's on screen |
| 75 | +3. **Efficient Updates**: We can compare the two trees to figure out exactly what changed |
| 76 | + |
| 77 | +## The Rendering Pipeline |
| 78 | + |
| 79 | +Rendering happens in two distinct phases that have very different characteristics: |
| 80 | + |
| 81 | +### Render Phase (Interruptible) |
| 82 | + |
| 83 | +This is where we build the work-in-progress tree. It's pure and has no side effects, which means we can pause it, throw it away, or restart it without any consequences. |
| 84 | + |
| 85 | +The render phase walks through the tree doing two main things: |
| 86 | + |
| 87 | +1. **Begin Work**: Process each fiber, call component functions, reconcile children |
| 88 | +2. **Complete Work**: Create or update DOM nodes, build effect lists |
| 89 | + |
| 90 | +The key insight here is that we're not touching the real DOM yet. We're just preparing a plan for what needs to change. |
| 91 | + |
| 92 | +### Commit Phase (Synchronous and Atomic) |
| 93 | + |
| 94 | +Once the render phase is done, we move to the commit phase. This is where we actually apply changes to the DOM. Unlike the render phase, this must complete without interruption. It happens in three sub-phases: |
| 95 | + |
| 96 | +1. **Before Mutation**: Take any snapshots we need (future work for getSnapshotBeforeUpdate) |
| 97 | +2. **Mutation**: Apply all DOM changes - insertions, updates, deletions |
| 98 | +3. **Layout**: Run layout effects, attach refs |
| 99 | + |
| 100 | +After the commit phase completes, users see the updated UI and the work-in-progress tree becomes the new current tree. |
| 101 | + |
| 102 | +## Effect Lists |
| 103 | + |
| 104 | +One of the clever optimizations in the fiber architecture is the effect list. During the render phase, as we complete each fiber, we build a linked list of all fibers that have side effects (things that need placement, updates, or deletion). |
| 105 | + |
| 106 | +When we get to the commit phase, instead of walking the entire tree again, we just walk this effect list. This means if you have a tree with 10,000 nodes but only 5 changed, we only process those 5 nodes during commit. |
| 107 | + |
| 108 | +```typescript |
| 109 | +// Each fiber can point to the next effect |
| 110 | +fiber.nextEffect = anotherFiber |
| 111 | + |
| 112 | +// The root tracks the first and last effects |
| 113 | +root.firstEffect = firstEffectFiber |
| 114 | +root.lastEffect = lastEffectFiber |
| 115 | +``` |
| 116 | + |
| 117 | +## Tree Traversal Without Recursion |
| 118 | + |
| 119 | +Traditional recursive tree traversal can blow the stack with deep trees. Fiber solves this by using the sibling pointers to implement depth-first traversal iteratively. |
| 120 | + |
| 121 | +Here's the basic pattern: |
| 122 | + |
| 123 | +```typescript |
| 124 | +function workLoop() { |
| 125 | + while (workInProgress !== null) { |
| 126 | + performUnitOfWork(workInProgress) |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +function performUnitOfWork(fiber) { |
| 131 | + // Process this fiber |
| 132 | + const next = beginWork(fiber) |
| 133 | + |
| 134 | + if (next) { |
| 135 | + // Has children, go to first child |
| 136 | + workInProgress = next |
| 137 | + } else { |
| 138 | + // No children, complete this fiber |
| 139 | + completeUnitOfWork(fiber) |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +function completeUnitOfWork(fiber) { |
| 144 | + while (fiber) { |
| 145 | + completeWork(fiber) |
| 146 | + |
| 147 | + if (fiber.sibling) { |
| 148 | + // Move to sibling |
| 149 | + workInProgress = fiber.sibling |
| 150 | + return |
| 151 | + } |
| 152 | + |
| 153 | + // No sibling, go back to parent |
| 154 | + fiber = fiber.return |
| 155 | + } |
| 156 | + |
| 157 | + workInProgress = null |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +This gives us the same depth-first order as recursion but without the stack depth issues. |
| 162 | + |
| 163 | +## Fiber Types |
| 164 | + |
| 165 | +Different fiber types get handled differently: |
| 166 | + |
| 167 | +**Host Components** (like 'div', 'span'): These create actual DOM nodes. During complete work, we call `document.createElement` and set up properties. |
| 168 | + |
| 169 | +**Function Components**: These don't create DOM nodes themselves. During begin work, we call the function and reconcile its return value. |
| 170 | + |
| 171 | +**Text Elements**: Special host components that create text nodes with `document.createTextNode`. |
| 172 | + |
| 173 | +**Fragments**: Don't create any DOM nodes, just a container for grouping children. |
| 174 | + |
| 175 | +**Portals**: Create a different rendering context, allowing children to render into a different DOM container. |
| 176 | + |
| 177 | +**Context Providers**: Update context values for their subtree. |
| 178 | + |
| 179 | +## Work-in-Progress Creation |
| 180 | + |
| 181 | +When we create a work-in-progress fiber from a current fiber, we try to reuse as much as possible: |
| 182 | + |
| 183 | +```typescript |
| 184 | +function createWorkInProgress(current, pendingProps) { |
| 185 | + let workInProgress = current.alternate |
| 186 | + |
| 187 | + if (workInProgress === null) { |
| 188 | + // First update, create a new fiber |
| 189 | + workInProgress = createFiber(current.type, pendingProps, current.key) |
| 190 | + workInProgress.alternate = current |
| 191 | + current.alternate = workInProgress |
| 192 | + } else { |
| 193 | + // Reuse the existing alternate |
| 194 | + workInProgress.pendingProps = pendingProps |
| 195 | + workInProgress.effectTag = null |
| 196 | + workInProgress.nextEffect = null |
| 197 | + workInProgress.firstEffect = null |
| 198 | + workInProgress.lastEffect = null |
| 199 | + } |
| 200 | + |
| 201 | + // Copy over state from current |
| 202 | + workInProgress.child = current.child |
| 203 | + workInProgress.memoizedProps = current.memoizedProps |
| 204 | + workInProgress.memoizedState = current.memoizedState |
| 205 | + // ... copy other fields |
| 206 | + |
| 207 | + return workInProgress |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +This pooling strategy reduces garbage collection pressure. |
| 212 | + |
| 213 | +## Priority and Scheduling |
| 214 | + |
| 215 | +Currently, all work is synchronous and happens in one go. But the architecture is set up to support different priority levels: |
| 216 | + |
| 217 | +```typescript |
| 218 | +// Future work |
| 219 | +const lanes = { |
| 220 | + NoLane: 0, |
| 221 | + SyncLane: 1, |
| 222 | + InputContinuousLane: 2, |
| 223 | + DefaultLane: 4, |
| 224 | + IdleLane: 8 |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +The idea is that urgent updates (like typing in an input) would get higher priority than non-urgent updates (like data fetching results). The fiber structure makes this possible because we can pause low-priority work to handle high-priority updates. |
| 229 | + |
| 230 | +## Memory Management |
| 231 | + |
| 232 | +The fiber tree can get pretty large, so memory management matters. A few strategies we use: |
| 233 | + |
| 234 | +**Reuse Fibers**: The alternate pattern means we're not constantly allocating new fibers |
| 235 | + |
| 236 | +**Clear Effect Lists**: After commit, we clear out all the effect pointers so garbage collection can happen |
| 237 | + |
| 238 | +**Minimal Cloning**: We only clone what changed, not the entire tree |
| 239 | + |
| 240 | +**WeakMaps**: For things like event system integration, we use WeakMaps so fibers can be garbage collected when no longer needed |
| 241 | + |
| 242 | +## Putting It All Together |
| 243 | + |
| 244 | +When you call `render()`: |
| 245 | + |
| 246 | +1. Create or get the fiber root for the container |
| 247 | +2. Update the root fiber's pending props with the new element |
| 248 | +3. Schedule an update by calling the work loop |
| 249 | +4. The work loop builds the work-in-progress tree |
| 250 | +5. During the render phase, we figure out what changed |
| 251 | +6. During the commit phase, we apply those changes to the DOM |
| 252 | +7. The work-in-progress tree becomes the new current tree |
| 253 | + |
| 254 | +The whole system is designed around making this process efficient and interruptible. The fiber architecture gives us the foundation to add features like time-slicing, Suspense, and concurrent rendering down the line. |
0 commit comments