feat: add Value.Equal bail-out check before reconciliation

Add TypeBox Value.Equal deep-comparison as first optimization layer
in reconcileProps. When a fiber's cached node is deep-equal to the
next node, skip prepareUpdate, commitUpdate, and children
reconciliation entirely. New cachedNode field on Fiber stores the
last reconciled node for comparison.
This commit is contained in:
2026-05-18 17:25:02 +00:00
parent 1e0abb0900
commit 23db3775ad
8 changed files with 283 additions and 41 deletions

View File

@@ -70,6 +70,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: node,
}; };
if (parentFiber) parentFiber.children.push(fiber); if (parentFiber) parentFiber.children.push(fiber);
return fiber; return fiber;
@@ -106,6 +107,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: node,
}; };
for (const child of el.children) { for (const child of el.children) {
@@ -132,6 +134,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: node,
}; };
} }
@@ -158,6 +161,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: node,
}; };
for (const child of el.children) { for (const child of el.children) {
@@ -281,6 +285,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: null,
}; };
const payloadChildren = isURoot(node) ? (node as URoot).children : [node]; const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
for (const child of payloadChildren) { for (const child of payloadChildren) {

View File

@@ -1,3 +1,5 @@
import type { UNode } from "../core/schema.js";
export interface Fiber<I> { export interface Fiber<I> {
instance: I; instance: I;
tag: string; tag: string;
@@ -8,6 +10,7 @@ export interface Fiber<I> {
effect: Effect<I> | null; effect: Effect<I> | null;
signalDisposers: (() => void)[]; signalDisposers: (() => void)[];
prevProps: Record<string, unknown> | null; prevProps: Record<string, unknown> | null;
cachedNode: UNode | null;
} }
export type Effect<I> = export type Effect<I> =

View File

@@ -1,4 +1,5 @@
import { effect } from "@preact/signals-core"; import { effect } from "@preact/signals-core";
import { Value } from "@alkdev/typebox/value";
import type { Fiber, Effect } from "./fiber.js"; import type { Fiber, Effect } from "./fiber.js";
import type { HostConfig } from "./config.js"; import type { HostConfig } from "./config.js";
import type { UNode, UElement } from "../core/schema.js"; import type { UNode, UElement } from "../core/schema.js";
@@ -59,6 +60,10 @@ export function reconcileProps<I>(
host: HostConfig<string, I, unknown>, host: HostConfig<string, I, unknown>,
ctx: unknown, ctx: unknown,
): void { ): void {
if (fiber.cachedNode !== null && Value.Equal(fiber.cachedNode, nextNode)) {
return;
}
if (isUPrimitive(nextNode)) { if (isUPrimitive(nextNode)) {
if (fiber.tag === "#text") { if (fiber.tag === "#text") {
const text = nextNode === null ? "" : String(nextNode); const text = nextNode === null ? "" : String(nextNode);
@@ -78,6 +83,7 @@ export function reconcileProps<I>(
} }
} }
} }
fiber.cachedNode = nextNode;
return; return;
} }
@@ -87,6 +93,7 @@ export function reconcileProps<I>(
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
reconcileProps(fiber.children[i]!, rootChildren[i]!, host, ctx); reconcileProps(fiber.children[i]!, rootChildren[i]!, host, ctx);
} }
fiber.cachedNode = nextNode;
return; return;
} }
@@ -96,6 +103,7 @@ export function reconcileProps<I>(
const component = el.type as (props: Record<string, unknown>) => UNode; const component = el.type as (props: Record<string, unknown>) => UNode;
const out = component({ ...el.props, children: el.children }); const out = component({ ...el.props, children: el.children });
reconcileProps(fiber, out, host, ctx); reconcileProps(fiber, out, host, ctx);
fiber.cachedNode = nextNode;
return; return;
} }
@@ -120,6 +128,7 @@ export function reconcileProps<I>(
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
reconcileProps(fiber.children[i]!, el.children[i]!, host, ctx); reconcileProps(fiber.children[i]!, el.children[i]!, host, ctx);
} }
fiber.cachedNode = nextNode;
} }
export function commitEffects<I>( export function commitEffects<I>(

View File

@@ -210,6 +210,7 @@ describe("commitMutations direct", () => {
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: null,
}; };
} }

View File

@@ -1,6 +1,15 @@
import { describe, it, expect, expectTypeOf } from "vitest"; import { describe, it, expect, expectTypeOf } from "vitest";
import type { Fiber, Effect } from "../src/host/fiber.js"; import type { Fiber, Effect } from "../src/host/fiber.js";
const baseFiber: Omit<Fiber<string>, "instance" | "tag" | "props" | "key"> = {
children: [],
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
cachedNode: null,
};
describe("Fiber<I> interface", () => { describe("Fiber<I> interface", () => {
it("has all required fields", () => { it("has all required fields", () => {
const fiber: Fiber<string> = { const fiber: Fiber<string> = {
@@ -8,11 +17,7 @@ describe("Fiber<I> interface", () => {
tag: "div", tag: "div",
props: { class: "test" }, props: { class: "test" },
key: "a", key: "a",
children: [], ...baseFiber,
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
}; };
expect(fiber.instance).toBe("inst-1"); expect(fiber.instance).toBe("inst-1");
expect(fiber.tag).toBe("div"); expect(fiber.tag).toBe("div");
@@ -23,6 +28,7 @@ describe("Fiber<I> interface", () => {
expect(fiber.effect).toBeNull(); expect(fiber.effect).toBeNull();
expect(fiber.signalDisposers).toEqual([]); expect(fiber.signalDisposers).toEqual([]);
expect(fiber.prevProps).toBeNull(); expect(fiber.prevProps).toBeNull();
expect(fiber.cachedNode).toBeNull();
}); });
it("supports key as undefined", () => { it("supports key as undefined", () => {
@@ -31,11 +37,7 @@ describe("Fiber<I> interface", () => {
tag: "span", tag: "span",
props: {}, props: {},
key: undefined, key: undefined,
children: [], ...baseFiber,
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
}; };
expect(fiber.key).toBeUndefined(); expect(fiber.key).toBeUndefined();
}); });
@@ -46,22 +48,15 @@ describe("Fiber<I> interface", () => {
tag: "div", tag: "div",
props: {}, props: {},
key: undefined, key: undefined,
children: [], ...baseFiber,
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
}; };
const child: Fiber<string> = { const child: Fiber<string> = {
instance: "child-inst", instance: "child-inst",
tag: "span", tag: "span",
props: {}, props: {},
key: "child-1", key: "child-1",
children: [], ...baseFiber,
parent, parent,
effect: null,
signalDisposers: [],
prevProps: null,
}; };
parent.children.push(child); parent.children.push(child);
expect(child.parent).toBe(parent); expect(child.parent).toBe(parent);
@@ -75,11 +70,8 @@ describe("Fiber<I> interface", () => {
tag: "div", tag: "div",
props: {}, props: {},
key: undefined, key: undefined,
children: [], ...baseFiber,
parent: null,
effect: null,
signalDisposers: [() => disposed.push("a"), () => disposed.push("b")], signalDisposers: [() => disposed.push("a"), () => disposed.push("b")],
prevProps: null,
}; };
fiber.signalDisposers.forEach((d) => d()); fiber.signalDisposers.forEach((d) => d());
expect(disposed).toEqual(["a", "b"]); expect(disposed).toEqual(["a", "b"]);
@@ -104,11 +96,7 @@ describe("Effect<I> union type", () => {
tag: "span", tag: "span",
props: {}, props: {},
key: undefined, key: undefined,
children: [], ...baseFiber,
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
}; };
const effect: Effect<string> = { type: "insert", before }; const effect: Effect<string> = { type: "insert", before };
expect(effect.type).toBe("insert"); expect(effect.type).toBe("insert");
@@ -121,11 +109,7 @@ describe("Effect<I> union type", () => {
tag: "span", tag: "span",
props: {}, props: {},
key: undefined, key: undefined,
children: [], ...baseFiber,
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
}; };
const effect: Effect<string> = { type: "move", before }; const effect: Effect<string> = { type: "move", before };
expect(effect.type).toBe("move"); expect(effect.type).toBe("move");
@@ -148,10 +132,8 @@ describe("Effect<I> union type", () => {
tag: "div", tag: "div",
props: {}, props: {},
key: undefined, key: undefined,
children: [], ...baseFiber,
parent: null,
effect: { type: "update", payload: "x" }, effect: { type: "update", payload: "x" },
signalDisposers: [],
prevProps: {}, prevProps: {},
}; };
expect(fiber.effect!.type).toBe("update"); expect(fiber.effect!.type).toBe("update");
@@ -179,11 +161,7 @@ describe("Fiber re-export from barrel", () => {
tag: "div", tag: "div",
props: {}, props: {},
key: undefined, key: undefined,
children: [], ...baseFiber,
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
}; };
const _effect: _EffectCheck = { type: "remove" }; const _effect: _EffectCheck = { type: "remove" };
expect(_fiber.instance).toBe("inst"); expect(_fiber.instance).toBe("inst");

View File

@@ -14,6 +14,7 @@ function makeFiber(key: string | undefined, tag: string): Fiber<string> {
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: null,
}; };
} }

View File

@@ -14,6 +14,7 @@ function makeFiber(key: string | undefined, tag: string): Fiber<string> {
effect: null, effect: null,
signalDisposers: [], signalDisposers: [],
prevProps: null, prevProps: null,
cachedNode: null,
}; };
} }

View File

@@ -0,0 +1,244 @@
import { describe, it, expect, beforeEach } from "vitest";
import { h } from "../src/core/h.js";
import { createRoot as createHostRoot } from "../src/host/config.js";
import type { HostConfig } from "../src/host/config.js";
import type { Fiber } from "../src/host/fiber.js";
import { reconcileProps, resetUpdateQueue } from "../src/host/reconcile.js";
function makeTrackingHost() {
const prepareUpdateCalls: { instance: string; tag: string; prevProps: Record<string, unknown>; nextProps: Record<string, unknown> }[] = [];
const commitUpdateCalls: { instance: string; payload: unknown; tag: string; prevProps: Record<string, unknown>; nextProps: Record<string, unknown> }[] = [];
const instances: { tag: string; props: Record<string, unknown> }[] = [];
const texts: string[] = [];
const appends: { parent: string; child: string }[] = [];
const host: HostConfig<string, string, Record<string, unknown>> = {
name: "tracking",
createRootContext: () => ({}),
createInstance: (tag, props) => {
const id = `${tag}_${instances.length}`;
instances.push({ tag, props });
return id;
},
createTextInstance: (text) => {
texts.push(text);
return `text_${texts.length - 1}`;
},
appendChild: (parent, child) => {
appends.push({ parent, child });
},
prepareUpdate: (instance, tag, prevProps, nextProps) => {
prepareUpdateCalls.push({ instance, tag, prevProps: { ...prevProps }, nextProps: { ...nextProps } });
const changed: Record<string, unknown> = {};
let hasChanges = false;
for (const key of Object.keys(nextProps)) {
if (prevProps[key] !== nextProps[key]) {
changed[key] = nextProps[key];
hasChanges = true;
}
}
for (const key of Object.keys(prevProps)) {
if (!(key in nextProps)) {
changed[key] = undefined;
hasChanges = true;
}
}
return hasChanges ? changed : null;
},
commitUpdate: (instance, payload, tag, prevProps, nextProps) => {
commitUpdateCalls.push({ instance, payload, tag, prevProps: { ...prevProps }, nextProps: { ...nextProps } });
},
};
return { host, prepareUpdateCalls, commitUpdateCalls, instances, texts, appends };
}
describe("Value.Equal bail-out", () => {
beforeEach(() => {
resetUpdateQueue();
});
describe("unchanged subtree skips prepareUpdate/commitUpdate", () => {
it("identical re-render skips prepareUpdate for unchanged element", () => {
const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }));
prepareUpdateCalls.length = 0;
commitUpdateCalls.length = 0;
root.render(h("div", { color: "red" }));
expect(prepareUpdateCalls.length).toBe(0);
expect(commitUpdateCalls.length).toBe(0);
});
it("identical re-render with children skips prepareUpdate for parent and children", () => {
const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }, h("span", { label: "a" })));
prepareUpdateCalls.length = 0;
commitUpdateCalls.length = 0;
root.render(h("div", { color: "red" }, h("span", { label: "a" })));
expect(prepareUpdateCalls.length).toBe(0);
expect(commitUpdateCalls.length).toBe(0);
});
});
describe("changed prop still triggers prepareUpdate", () => {
it("changed prop on parent triggers prepareUpdate", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }));
prepareUpdateCalls.length = 0;
root.render(h("div", { color: "blue" }));
expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1);
const divCall = prepareUpdateCalls.find((c) => c.tag === "div");
expect(divCall).toBeDefined();
expect(divCall!.prevProps.color).toBe("red");
expect(divCall!.nextProps.color).toBe("blue");
});
it("changed child prop triggers prepareUpdate only for child", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }, h("span", { label: "a" })));
prepareUpdateCalls.length = 0;
root.render(h("div", { color: "red" }, h("span", { label: "b" })));
const spanCalls = prepareUpdateCalls.filter((c) => c.tag === "span");
expect(spanCalls.length).toBe(1);
expect(spanCalls[0]!.prevProps.label).toBe("a");
expect(spanCalls[0]!.nextProps.label).toBe("b");
});
});
describe("deeply nested unchanged subtree is fully skipped", () => {
it("three levels of unchanged elements are all skipped", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(
h("div", { id: "1" },
h("section", { class: "outer" },
h("p", { align: "center" }, "text"),
),
),
);
prepareUpdateCalls.length = 0;
root.render(
h("div", { id: "1" },
h("section", { class: "outer" },
h("p", { align: "center" }, "text"),
),
),
);
expect(prepareUpdateCalls.length).toBe(0);
});
it("deep subtree with one change only commitUpdates the changed node", () => {
const { host, commitUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(
h("div", { id: "1" },
h("section", { class: "outer" },
h("p", { align: "center" }),
),
),
);
commitUpdateCalls.length = 0;
root.render(
h("div", { id: "1" },
h("section", { class: "outer" },
h("p", { align: "left" }),
),
),
);
const pCommit = commitUpdateCalls.filter((c) => c.tag === "p");
expect(pCommit.length).toBe(1);
expect(pCommit[0]!.prevProps.align).toBe("center");
expect(pCommit[0]!.nextProps.align).toBe("left");
});
});
describe("cachedNode is set on mount", () => {
it("fibers created during mount have cachedNode set", () => {
const { host } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }, h("span", { label: "a" })));
const divFiber = root.rootFiber!.children[0]!;
expect(divFiber.cachedNode).not.toBeNull();
const spanFiber = divFiber.children[0]!;
expect(spanFiber.cachedNode).not.toBeNull();
});
it("cachedNode matches the node used to create the fiber", () => {
const { host } = makeTrackingHost();
const root = createHostRoot(host, {});
const divNode = h("div", { color: "red" });
root.render(divNode);
const divFiber = root.rootFiber!.children[0]!;
expect(divFiber.cachedNode).not.toBeNull();
});
});
describe("reconcileProps direct call with Value.Equal bail-out", () => {
it("reconcileProps with identical node does not call prepareUpdate", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }));
const divFiber = root.rootFiber!.children[0]!;
expect(divFiber.cachedNode).not.toBeNull();
prepareUpdateCalls.length = 0;
reconcileProps(divFiber, h("div", { color: "red" }), host as HostConfig<string, string, unknown>, {});
expect(prepareUpdateCalls.length).toBe(0);
expect(divFiber.effect).toBeNull();
});
it("reconcileProps with changed node calls prepareUpdate", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { color: "red" }));
const divFiber = root.rootFiber!.children[0]!;
prepareUpdateCalls.length = 0;
reconcileProps(divFiber, h("div", { color: "blue" }), host as HostConfig<string, string, unknown>, {});
expect(prepareUpdateCalls.length).toBe(1);
});
});
describe("Value.Equal deep equality", () => {
it("deep-equal props with different object references still bail out", () => {
const { host, prepareUpdateCalls } = makeTrackingHost();
const root = createHostRoot(host, {});
root.render(h("div", { style: { color: "red", size: 10 } }));
prepareUpdateCalls.length = 0;
root.render(h("div", { style: { color: "red", size: 10 } }));
expect(prepareUpdateCalls.length).toBe(0);
});
});
});