/** * Memo hook benchmark: many memo cells in one resource. Measures stable deps, * changing deps, and React Compiler-style memo caches. * * pnpm build --filter=@assistant-ui/tap * pnpm --dir packages/tap exec vitest bench --run --project prod src/__tests__/bench/memo-hooks.bench.tsx */ /* oxlint-disable react/rules-of-hooks -- fixed-count hook loops, benchmark only */ import { bench, describe } from "vitest"; import { createElement, useMemo, useState } from "react"; import { createRoot } from "react-dom/client"; import { flushSync } from "react-dom"; import { createTapRoot, flushTapSync } from "@assistant-ui/tap"; import { c } from "../../react-shim/compiler-runtime"; import { useRenderMemo } from "../../hooks/utils/useRenderMemo"; (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false; const N = 1_000; const MEMO_CACHE_SENTINEL = Symbol.for("react.memo_cache_sentinel"); type Host = { bump: () => void; unmount: () => void; }; const tapUseMemoStable = (): Host => { let setTick!: (updater: (value: number) => number) => void; const root = createTapRoot(function Root() { const [, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { sum += useMemo(() => i, [i]); } return sum; }); return { bump: () => flushTapSync(() => setTick((v) => v + 1)), unmount: () => root.unmount(), }; }; const tapUseMemoChanging = (): Host => { let setTick!: (updater: (value: number) => number) => void; const root = createTapRoot(function Root() { const [tick, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { sum += useMemo(() => i + tick, [i, tick]); } return sum; }); return { bump: () => flushTapSync(() => setTick((v) => v + 1)), unmount: () => root.unmount(), }; }; const tapCStable = (): Host => { let setTick!: (updater: (value: number) => number) => void; const root = createTapRoot(function Root() { const [, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { const $ = c(2); if ($[0] === MEMO_CACHE_SENTINEL) { $[0] = i; $[1] = i; } sum += $[1] as number; } return sum; }); return { bump: () => flushTapSync(() => setTick((v) => v + 1)), unmount: () => root.unmount(), }; }; const reactUseMemoStable = (): Host => { let setTick!: (updater: (value: number) => number) => void; function Root() { const [, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { sum += useMemo(() => i, [i]); } return sum; } const root = createRoot(document.createElement("div")); flushSync(() => root.render(createElement(Root))); return { bump: () => flushSync(() => setTick((v) => v + 1)), unmount: () => flushSync(() => root.unmount()), }; }; const reactUseMemoChanging = (): Host => { let setTick!: (updater: (value: number) => number) => void; function Root() { const [tick, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { sum += useMemo(() => i + tick, [i, tick]); } return sum; } const root = createRoot(document.createElement("div")); flushSync(() => root.render(createElement(Root))); return { bump: () => flushSync(() => setTick((v) => v + 1)), unmount: () => flushSync(() => root.unmount()), }; }; const reactCStable = (): Host => { let setTick!: (updater: (value: number) => number) => void; function Root() { const [, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { const $ = c(2); if ($[0] === MEMO_CACHE_SENTINEL) { $[0] = i; $[1] = i; } sum += $[1] as number; } return sum; } const root = createRoot(document.createElement("div")); flushSync(() => root.render(createElement(Root))); return { bump: () => flushSync(() => setTick((v) => v + 1)), unmount: () => flushSync(() => root.unmount()), }; }; const reactUseRenderMemoStable = (): Host => { let setTick!: (updater: (value: number) => number) => void; function Root() { const [, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { sum += useRenderMemo(() => i, [i], false); } return sum; } const root = createRoot(document.createElement("div")); flushSync(() => root.render(createElement(Root))); return { bump: () => flushSync(() => setTick((v) => v + 1)), unmount: () => flushSync(() => root.unmount()), }; }; const tapUseRenderMemoStable = (): Host => { let setTick!: (updater: (value: number) => number) => void; const root = createTapRoot(function Root() { const [, set] = useState(0); setTick = set; let sum = 0; for (let i = 0; i < N; i++) { sum += useRenderMemo(() => i, [i], false); } return sum; }); return { bump: () => flushTapSync(() => setTick((v) => v + 1)), unmount: () => root.unmount(), }; }; describe(`memo hooks stable update, ${N} cells`, () => { const hosts = { "react:useMemo": reactUseMemoStable, "tap:useMemo": tapUseMemoStable, "react:c": reactCStable, "tap:c": tapCStable, "react:useRenderMemo": reactUseRenderMemoStable, "tap:useRenderMemo": tapUseRenderMemoStable, }; for (const [name, make] of Object.entries(hosts)) { let host: Host; bench(name, () => host.bump(), { setup: () => { host = make(); }, teardown: () => host.unmount(), }); } }); describe(`memo hooks changing update, ${N} cells`, () => { const hosts = { "react:useMemo": reactUseMemoChanging, "tap:useMemo": tapUseMemoChanging, }; for (const [name, make] of Object.entries(hosts)) { let host: Host; bench(name, () => host.bump(), { setup: () => { host = make(); }, teardown: () => host.unmount(), }); } });