Skip to content

Commit a9b1933

Browse files
committed
✨ feat: implement fiber architecture with complete reconciliation engine and comprehensive documentation
Replaces the old recursive rendering system with React-like fiber architecture for better performance and future concurrent mode support, includes full test suite, modernized ElysiaJS example app, and detailed design documentation for all core systems.
1 parent 12c42ee commit a9b1933

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+13057
-3139
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,4 @@ dist
175175
.DS_Store
176176

177177
# Cursor
178-
.cursor
178+
.cursor

.trae/rules/project_rules.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

biome.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"**/bundle.js",
1414
"**/*.bundle.js",
1515
"**/node_modules/**",
16-
"**/.git/**"
16+
"**/.git/**",
17+
"example/public/**"
1718
]
1819
},
1920
"formatter": {
@@ -33,9 +34,24 @@
3334
},
3435
"security": {
3536
"noGlobalEval": "warn"
37+
},
38+
"style": {
39+
"noNonNullAssertion": "error"
3640
}
3741
}
3842
},
43+
"overrides": [
44+
{
45+
"include": ["tests/**/*.test.ts"],
46+
"linter": {
47+
"rules": {
48+
"style": {
49+
"noNonNullAssertion": "off"
50+
}
51+
}
52+
}
53+
}
54+
],
3955
"javascript": {
4056
"formatter": {
4157
"quoteStyle": "double"

docs/01-fiber-architecture.md

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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

Comments
 (0)