How Fre Tracks Promises for Suspense Boundaries: WeakMap and Fiber Architecture
Fre tracks promises for Suspense boundaries by throwing promises from components during the render phase and catching them in the reconciler's updateHook function, then storing associations in a WeakMap named suspendPromiseMap that links each promise to its nearest Suspense boundary fiber.
Suspense enables declarative asynchronous rendering in modern frontend frameworks, allowing components to display fallback content while waiting for data or code to load. In Fre—a lightweight, React-compatible library—this feature relies on a sophisticated tracking mechanism that maps promises to fiber nodes in the component tree. Understanding how Fre tracks promises for Suspense boundaries requires examining the reconciler's throw-and-catch pattern and the WeakMap-based registry in src/reconcile.ts.
Throwing and Catching Promises During Render
Fre adopts the throw-and-catch pattern for Suspense implementation. When a component needs to suspend—typically through the lazy utility or by manually throwing a promise—it throws the promise object during its execution. The reconciler wraps component rendering in a try-catch block inside the updateHook function.
In src/reconcile.ts (lines 61-66), when a promise is caught, the reconciler immediately invokes suspenseRender to handle the suspension:
// Conceptual flow in reconcile.ts
try {
// Component renders
} catch (e) {
if (e instanceof Promise) {
suspenseRender(e, fiber)
}
}
This interruption prevents the component tree from completing its render until the promise resolves.
The Role of lazy Components
The lazy function, defined in src/h.ts, creates components that automatically throw the promise returned by dynamic import functions. When the lazy component renders before its module has loaded, it throws the import promise, triggering the catch block in updateHook and initiating the Suspense workflow.
Locating the Nearest Suspense Boundary
Once suspenseRender receives a thrown promise, it must identify which Suspense boundary should handle it. Fre traverses the fiber tree upward using the getBoundary function, searching for the nearest ancestor fiber with the Suspense type.
According to src/reconcile.ts (lines 46-53), this traversal walks from the current fiber to the root:
const getBoundary = (fiber, type) => {
while (fiber) {
if (fiber.type === type) return fiber
fiber = fiber.parent
}
}
This ensures that nested Suspense boundaries handle only the promises thrown by their descendants, allowing fine-grained loading states.
The WeakMap Tracking Mechanism
The core of Fre's promise tracking lies in suspendPromiseMap, a WeakMap<Promise, Set<Fiber>> defined in the reconciler. This structure maps each unique promise to a Set of Suspense boundary fibers that are waiting for it.
As implemented in src/reconcile.ts (lines 75-83), the registration logic works as follows:
- If the promise is new to the map, Fre creates a new
Setcontaining the boundary fiber and attaches a.thenlistener to the promise. - If the promise already exists in the map (indicating another component or boundary is waiting for the same async operation), Fre simply adds the new boundary to the existing Set.
// Simplified from reconcile.ts
if (!suspendPromiseMap.has(promise)) {
suspendPromiseMap.set(promise, new Set([boundary]))
promise.then(() => {
// Resolution handler
}).finally(() => {
suspendPromiseMap.delete(promise)
})
} else {
suspendPromiseMap.get(promise).add(boundary)
}
Using a WeakMap ensures that resolved promises remain eligible for garbage collection once all references are cleared, preventing memory leaks in long-running applications.
Promise Resolution and Boundary Updates
When a tracked promise resolves, the .then handler retrieves the Set<Fiber> from suspendPromiseMap. Before triggering updates, Fre filters out boundaries that are no longer valid—specifically those marked with TAG.REPLACE or TAG.REMOVE flags defined in src/type.ts.
As shown in src/reconcile.ts (lines 78-82), the resolution handler iterates through the boundary set and schedules updates only for alive fibers:
const boundaries = suspendPromiseMap.get(promise)
boundaries.forEach(boundary => {
if (!(boundary.tag & (TAG.REPLACE | TAG.REMOVE))) {
update(boundary)
}
})
This filtering prevents unnecessary re-renders of components that have been unmounted or replaced while the promise was pending.
Swapping Fallback for Primary Content
During the initial suspension, suspenseRender immediately renders the fallback UI while keeping the primary children in an off-screen fragment using MODE.OFFSCREEN. When the promise resolves and update triggers the re-render, suspenseRender (lines 64-74 in src/reconcile.ts) swaps these fragments.
The function returns boundary.child, allowing the reconciler to proceed with rendering the actual component tree instead of the fallback. This transition occurs seamlessly because the promise resolution has guaranteed that the suspended component can now render without throwing.
Practical Implementation with Lazy Loading
To see promise tracking in action, consider a typical lazy loading pattern:
// demo/src/suspense.tsx
import { Suspense, lazy } from 'fre';
const AsyncComp = lazy(() => import('./async-comp'));
export default function App() {
return (
<Suspense fallback={<p>Loading…</p>}>
<AsyncComp />
</Suspense>
);
}
In this example, AsyncComp throws the dynamic import promise on its first render. The reconciler catches this promise, registers the <Suspense> boundary in suspendPromiseMap, and renders "Loading…". Once the import resolves, the boundary updates and Fre renders the loaded component.
Summary
- Fre intercepts promises thrown during component rendering in the
updateHooktry-catch block located insrc/reconcile.ts(lines 61-66). - The framework locates the nearest Suspense boundary by traversing the fiber tree upward via
getBoundary. - Promise-to-boundary associations are stored in
suspendPromiseMap, aWeakMapmapping promises to Sets of fiber nodes, supporting multiple boundaries per promise. - When promises resolve, Fre filters out stale boundaries using TAG flags before calling
updateto schedule re-renders. - The reconciler swaps off-screen fragments for the primary content once promises settle, completing the Suspense transition.
Frequently Asked Questions
What data structure does Fre use to track Suspense promises?
Fre uses a WeakMap<Promise, Set<Fiber>> named suspendPromiseMap. The WeakMap keys are the thrown promises, and the values are Sets containing one or more Suspense boundary fibers that are waiting for that specific promise to resolve.
How does Fre prevent memory leaks when tracking promises?
The use of a WeakMap ensures that promise references do not prevent garbage collection. Once a promise resolves and the .finally handler deletes the entry from suspendPromiseMap, the promise becomes eligible for garbage collection if no other references exist. Additionally, resolved promises are automatically removed from the map after triggering boundary updates.
Can multiple Suspense boundaries track the same promise simultaneously?
Yes. When a promise is thrown within a component tree containing nested Suspense boundaries, or when multiple components throw the same promise reference, Fre adds each relevant boundary fiber to the Set associated with that promise in suspendPromiseMap. When the promise resolves, all boundaries in the Set receive update notifications.
How does Fre determine if a Suspense boundary is still valid when a promise resolves?
Before re-rendering boundaries, Fre checks the fiber's tag property against the TAG.REPLACE and TAG.REMOVE flags defined in src/type.ts. Boundaries marked with these flags—indicating they have been replaced or removed from the tree—are filtered out and skip the update, ensuring only active boundaries re-render when promises settle.
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 →