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.
244 lines
8.5 KiB
TypeScript
244 lines
8.5 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |