Reconciliation
Reconciliation is the process of efficiently updating the DOM based on changes to the virtual DOM. It’s a core part of what makes Fluxus performant.
How Reconciliation Works
When a component’s state or props change, Fluxus performs these steps:
- The component function is called again to produce a new virtual DOM tree
- This new tree is compared with the previous tree (diffing)
- A minimal set of operations is generated to transform the real DOM
- These operations are applied to the DOM in a single batch
The Diff Algorithm
The diffing algorithm in Fluxus is designed to be efficient with a few key assumptions:
-
Different node types will produce different trees: If an element changes from a
div
to aspan
, Fluxus rebuilds the entire subtree rather than trying to patch it. -
Stable elements can be identified with keys: When lists of elements change, keys help identify which items were added, removed, or moved.
-
Children are diffed recursively: The algorithm walks the tree recursively, comparing node by node.
DOM Operations
The reconciliation process generates a sequence of operations that are batched together for efficiency:
UpdateProperties
: Updates properties on an existing elementReplaceNode
: Replaces an entire node (and its children) with a new oneUpdateText
: Updates the content of a text nodeRemoveProps
: Removes properties from an elementAddProps
: Adds new properties to an elementRemoveEvent
: Removes an event handlerAddEvent
: Adds an event handlerRemoveNode
: Removes a node from the DOMInsertNode
: Inserts a new nodeRerenderComponent
: Re-renders a component with new propsMoveNode
: Moves a node to a new position
Key-Based Reconciliation
When rendering lists of elements, using the key
property helps Fluxus identify which items have changed:
ul(
items.map(item =>
li(key := item.id, item.text)
)
)
Without keys, Fluxus has to rebuild large portions of the DOM when items change position. With keys, it can identify moved elements and reuse DOM nodes, resulting in better performance.
Component Reconciliation
Components are reconciled based on their type and key:
- If a component’s type changes (different component function), the entire subtree is replaced
- If a component’s type stays the same but props change, the component instance is reused but re-rendered
- If both type and props are unchanged, the component can often skip re-rendering
Batching Updates
Fluxus batches multiple state updates that occur within the same event loop iteration. This means multiple calls to state setters will result in only one re-render:
val (count, setCount, _) = useState(0)
// These will be batched into a single update
button(
onClick := (() => {
setCount(count + 1)
setCount(count + 2)
})
)
However, functional updates will apply sequentially:
val (count, _, updateCount) = useState(0)
// These will apply sequentially, resulting in count = 2
button(
onClick := (() => {
updateCount(c => c + 1)
updateCount(c => c + 1)
})
)
Effect Handling
After the DOM has been updated, Fluxus runs effects (from useEffect
hooks). This ensures that effects always see the latest DOM state.
Effects are executed in order from parent to child components, to ensure a consistent data flow.
Implementation Details
The reconciliation process is implemented primarily in these modules:
diff.scala
: Generates operations by comparing virtual DOM treescommit.scala
: Applies operations to the real DOMBatchScheduler.scala
: Handles batching updates and scheduling effectsreconcile.scala
: Coordinates the overall reconciliation process