import { createElement, memo, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { flushSync } from "react-dom";
import { Button } from "@/components/ui/button.tsx";
import { Checkbox } from "@/components/ui/checkbox.tsx";
import { Typography } from "@/components/ui/typography.tsx";
import { useLogger } from "@/components/pages/tests/experiment.tsx";
// An icon as data, the shape a design-system export ships: [tag, attrs, children?].
type IconNode = readonly [tag: string, attrs: Record<string, string>, children?: readonly IconNode[]];
// A receipt with a check mark, two lines and two dots: about the size of a real icon.
const RECEIPT: readonly IconNode[] = [
["path", { d: "M6 3h12v18l-2-1.5-2 1.5-2-1.5-2 1.5-2-1.5L6 21z" }],
["g", { "data-part": "check" }, [["path", { d: "m9 8 2 2 4-4" }]]],
["path", { d: "M9 13h6" }],
["path", { d: "M9 16h3" }],
[
"g",
{ "data-part": "dots", fill: "currentColor", stroke: "none" },
[
["circle", { cx: "13", cy: "16", r: ".8" }],
["circle", { cx: "15", cy: "16", r: ".8" }],
],
],
];
function renderNodes(nodes: readonly IconNode[]): ReactNode[] {
return nodes.map(([tag, attrs, children], key) =>
createElement(tag, { key, ...attrs }, ...(children ? renderNodes(children) : []))
);
}
type IconProps = { icon: readonly IconNode[]; size?: number; style?: CSSProperties };
function Svg({ size = 14, style, children }: Omit<IconProps, "icon"> & { children: ReactNode }) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
style={style}
>
{children}
</svg>
);
}
// 1. Builds the drawing's elements again on every render.
function FreshIcon({ icon, ...props }: IconProps) {
return <Svg {...props}>{renderNodes(icon)}</Svg>;
}
// 2. Builds them once per icon and hands React the same elements on every render,
// so React skips the drawing and only checks the <svg> around it.
const drawings = new WeakMap<readonly IconNode[], ReactNode[]>();
function drawing(icon: readonly IconNode[]) {
let elements = drawings.get(icon);
if (!elements) drawings.set(icon, (elements = renderNodes(icon)));
return elements;
}
function CachedIcon({ icon, ...props }: IconProps) {
return <Svg {...props}>{drawing(icon)}</Svg>;
}
// 3. Skips the whole icon, but only while every prop keeps its identity.
const MemoIcon = memo(FreshIcon);
const VARIANTS = { fresh: FreshIcon, cached: CachedIcon, memo: MemoIcon };
type Variant = keyof typeof VARIANTS;
const COUNTS = [500, 1000, 2000];
const RERENDERS = 30;
function IconGrid({ variant, count, inlineStyle }: { variant: Variant; count: number; inlineStyle: boolean }) {
const Icon = VARIANTS[variant];
return (
<div className="flex flex-wrap gap-1 h-32 overflow-hidden text-subText" dir="ltr">
{Array.from({ length: count }, (_, i) => (
// With the checkbox on: a new style object on every render, as most call sites write it.
<Icon key={i} icon={RECEIPT} style={inlineStyle ? { opacity: 1 } : undefined} />
))}
</div>
);
}
const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve)));
export function IconDrawingCacheDemo() {
const { logTable } = useLogger();
const [count, setCount] = useState(1000);
const [inlineStyle, setInlineStyle] = useState(false);
const [grid, setGrid] = useState<{ variant: Variant; key: number } | null>(null);
const [, setTick] = useState(0);
const [running, setRunning] = useState(false);
const mounts = useRef(0);
// Mounts a fresh grid of the variant, then re-renders its parent RERENDERS times.
// The icons' props never change, so all the work is React deciding what to skip.
async function time(variant: Variant) {
await nextFrame();
let start = performance.now();
flushSync(() => setGrid({ variant, key: ++mounts.current }));
const mount = performance.now() - start;
// Timed together and averaged: one re-render is close to the timer's resolution.
start = performance.now();
for (let i = 0; i < RERENDERS; i++) flushSync(() => setTick((n) => n + 1));
return { variant, mount, rerender: (performance.now() - start) / RERENDERS };
}
async function measure() {
setRunning(true);
const variants = Object.keys(VARIANTS) as Variant[];
// One unmeasured pass first, so the first variant isn't timed while the JIT is still cold.
for (const variant of variants) await time(variant);
const results = [];
for (const variant of variants) results.push(await time(variant));
const fresh = results[0].rerender;
logTable(
results.map(({ variant, mount, rerender }) => ({
variant,
icons: String(count),
"mount ms": mount.toFixed(1),
"re-render ms": rerender.toFixed(2),
"per icon µs": ((rerender * 1000) / count).toFixed(2),
"vs fresh": `${(fresh / rerender).toFixed(1)}x`,
})),
`${count}-${inlineStyle ? "inline" : "stable"}`
);
setRunning(false);
}
return (
<div className="flex flex-col gap-5">
<div className="flex flex-wrap items-center gap-3">
<span className="text-[.85rem] text-subText">تعداد آیکون:</span>
{COUNTS.map((n) => (
<Button
key={n}
variant={n === count ? "primary" : "secondary"}
onClick={() => setCount(n)}
disabled={running}
className="font-mono"
>
{n}
</Button>
))}
</div>
<Checkbox
label="به هر آیکون یک style درونخطی هم بده (در هر رندر یک شیء تازه)"
checked={inlineStyle}
onChange={(e) => setInlineStyle(e.target.checked)}
disabled={running}
/>
<div className="flex flex-col gap-2">
<Button onClick={measure} disabled={running} className="self-start">
{running ? "در حال اندازهگیری…" : "اندازهگیری هر سه نسخه"}
</Button>
<Typography variant="faint" className="max-w-[var(--measure)]">
زمانها در مرورگر خودتان گرفته میشوند. در حالت توسعه، ریاکت کندتر است و StrictMode هر رندر را دو بار اجرا
میکند؛ پس به نسبتها نگاه کنید، نه به عددها.
</Typography>
</div>
{grid && <IconGrid key={grid.key} variant={grid.variant} count={count} inlineStyle={inlineStyle} />}
</div>
);
}
کش کردن رسم آیکون
§ کش کردن رسم آیکون
سه نسخه از یک کامپوننت آیکون که از داده ساخته میشود: یکی عناصر SVG را در هر رندر از نو میسازد، یکی آنها را یک بار میسازد و نگه میدارد، و یکی با memo پیچیده شده. با دکمهی اندازهگیری، هر سه mount میشوند و والدشان سی بار دوباره رندر میشود؛ زمانها در جدول خروجی میآیند.
پیشنمایش
تعداد آیکون:
زمانها در مرورگر خودتان گرفته میشوند. در حالت توسعه، ریاکت کندتر است و StrictMode هر رندر را دو بار اجرا میکند؛ پس به نسبتها نگاه کنید، نه به عددها.
خروجی
هنوز چیزی ثبت نشده.
کد