What Operations Does Fre’s Diff Algorithm Support? UPDATE, INSERT, MOVE, and REMOVE Explained

Fre’s virtual-DOM diff algorithm produces three explicit action types—UPDATE, INSERT, and MOVE—while performing REMOVE operations immediately via direct DOM cleanup rather than queuing them in the actions array.

Fre is a lightweight React-like framework maintained in the frejs/fre repository. Its reconciliation engine uses a specialized diff algorithm to minimize DOM mutations. Understanding which operations the Fre diff algorithm supports—and how it handles node removal—is essential for debugging render behavior and optimizing component performance.

The Four TAG Operations Defined in Fre

The TAG enum in src/type.ts (lines 116–124) defines four bit-flag constants that drive the reconciliation process. However, only three of these flags appear in the actions array returned by the diff function in src/reconcile.ts.

UPDATE (TAG.UPDATE)

Value: 1 << 1 (binary 10)

The UPDATE operation occurs when a previous fiber and the new fiber represent the same element—specifically when both the type and key match. The algorithm clones the old fiber onto the new one and pushes an {op: TAG.UPDATE} entry into the actions array. This signals the commit phase to reuse the existing DOM node and apply property changes.

INSERT (TAG.INSERT)

Value: 1 << 2 (binary 100)

INSERT actions are generated when a new element appears in the children list but has no matching counterpart in the previous render. The diff algorithm pushes {op: TAG.INSERT, cur: newFiber, ref: oldFiber}, instructing the commit phase to create a fresh DOM node and insert it before the reference fiber.

MOVE (TAG.MOVE)

Value: 1 << 6 (binary 1000000)

MOVE operations handle reordering. When the diff algorithm encounters an existing element later in the new list (identified by its key), it clones the old fiber onto the new one and emits {op: TAG.MOVE, cur: oldFiber, ref: oldFiberAtHead}. This moves the DOM node to its new position without destroying and recreating it.

REMOVE (TAG.REMOVE)

Value: Defined in the enum but not emitted by diff

Unlike UPDATE, INSERT, and MOVE, the REMOVE operation never appears in the actions array returned from the diff function. Instead, when an old fiber has no counterpart in the new children list, the algorithm calls removeElement(oldFiber) directly during the reconciliation loop. The TAG.REMOVE flag is later set on the removed fiber in src/commit.ts (line 64) to coordinate cleanup, but the diff phase itself does not generate a REMOVE action.

How the Diff Algorithm Generates Actions

The core diff logic resides in src/reconcile.ts (approximately lines 40–103). This implementation uses a while-loop to compare the old fiber list against the new fiber list, generating actions based on key matching and element type comparisons.

The algorithm prioritizes in-place updates for matching keys. When keys differ, it searches the remaining old fibers for a match. Finding a match triggers a MOVE action; failing to find one triggers an INSERT. Elements that remain unmatched in the old list after the loop completes are passed directly to removeElement for immediate destruction.

Practical Code Examples

Detecting a MOVE Operation

When sibling elements swap positions, Fre emits MOVE actions to reorder the DOM efficiently:

import { reconcile } from 'fre'

function Reorder({ swap }: { swap: boolean }) {
  return swap ? (
    <>
      <div key="b">B</div>
      <div key="a">A</div>
    </>
  ) : (
    <>
      <div key="a">A</div>
      <div key="b">B</div>
    </>
  )
}

// Initial render
const root = document.getElementById('root')
reconcile(root, <Reorder swap={false} />)

// Update triggers MOVE for both fibers
reconcile(root, <Reorder swap={true} />)

In src/reconcile.ts, the diff detects that keys "a" and "b" exist in both lists but in different positions. It clones the old fibers and pushes {op: TAG.MOVE, cur: oldFiber, ref: oldFiberAtHead} for each, moving the existing DOM nodes rather than recreating them.

Handling INSERT for New Elements

Adding a new child generates an INSERT action:

reconcile(root, (
  <>
    <span key="existing">First</span>
  </>
))

// Later...
reconcile(root, (
  <>
    <span key="existing">First</span>
    <span key="new">Second</span>  {/* INSERT action generated */}
  </>
))

Because the fiber with key "new" has no corresponding old fiber, the diff in src/reconcile.ts pushes {op: TAG.INSERT, cur: newFiber, ref: oldFiber}, causing the commit phase to append a new DOM node.

Processing UPDATE for Attribute Changes

When a component’s props change but its key and type remain constant, the algorithm issues an UPDATE:

reconcile(root, <input key="main" value="initial" />)

// Later...
reconcile(root, <input key="main" value="updated" />)

The matching key ("main") and type (input) cause the diff to clone the old fiber and emit {op: TAG.UPDATE}. The commit phase updates the value attribute on the existing DOM node without replacing the element.

REMOVE Handling Outside the Diff Loop

Deletions bypass the actions array entirely:

reconcile(root, (
  <>
    <p key="keep">Keep this</p>
    <p key="delete">Delete this</p>
  </>
))

// Later...
reconcile(root, (
  <>
    <p key="keep">Keep this</p>  {/* "delete" fiber removed immediately */}
  </>
))

When processing the second render, the diff loop in src/reconcile.ts finds no match for the "delete" key. Instead of creating a REMOVE action, it invokes removeElement(oldFiber) immediately. The fiber’s flag is set to TAG.REMOVE in src/commit.ts to ensure proper cleanup of effects and refs, but this happens outside the diff result.

Summary

  • UPDATE (1 << 1): Generated for matching type and key pairs, enabling in-place property updates in src/reconcile.ts.
  • INSERT (1 << 2): Generated for new fibers without corresponding old fibers, triggering DOM node creation.
  • MOVE (1 << 6): Generated when existing fibers change position, allowing efficient DOM reordering without node destruction.
  • REMOVE: Not emitted by the diff algorithm; handled via direct removeElement calls during reconciliation, with the TAG.REMOVE flag applied in src/commit.ts for cleanup coordination.

Frequently Asked Questions

Does Fre’s diff algorithm generate a REMOVE action in the actions array?

No. According to the source code in src/reconcile.ts, the diff loop calls removeElement(oldFiber) directly when an old fiber lacks a matching new fiber. The TAG.REMOVE flag is set later in src/commit.ts (line 64), but it never appears in the actions array returned from diff.

What distinguishes a MOVE operation from an UPDATE operation in Fre?

A MOVE operation (TAG.MOVE, 1 << 6) occurs when an element’s key and type match but its position in the children list changes, requiring DOM node repositioning. An UPDATE operation (TAG.UPDATE, 1 << 1) occurs when the element maintains the same position, requiring only attribute or content modifications.

How does Fre handle insertions of new elements during reconciliation?

When the diff algorithm encounters a new fiber with no matching old counterpart in src/reconcile.ts, it generates an {op: TAG.INSERT, cur: newFiber, ref: oldFiber} action. The commit phase then creates a new DOM node and inserts it before the reference fiber.

Where are the TAG constants defined in the Fre repository?

The TAG enum defining UPDATE, INSERT, REMOVE, and MOVE constants lives in src/type.ts at lines 116–124. The core logic that consumes these flags is implemented in src/reconcile.ts, while the actual DOM removal and flag application occurs in src/commit.ts.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →