Skip to main content

editor-perf

Guides performance investigation, benchmarking, and optimization of the Grida Canvas web editor (TypeScript reducer, Immer, React hooks). Use when profiling reducer dispatch cost, diagnosing slow interactions (drag, resize, color change), writing or running editor benchmarks, instrumenting with PerfObserver, or optimizing the JS-side state management pipeline.

ソース情報

リポジトリ
gridaco/grida
ソースの最終更新活動
2026年7月17日 14:08
検出された SKILL.md の言語
英語
スター
2,675
フォーク
150

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
editor-perf
description
Guides performance investigation, benchmarking, and optimization of the Grida Canvas web editor (TypeScript reducer, Immer, React hooks). Use when profiling reducer dispatch cost, diagnosing slow interactions (drag, resize, color change), writing or running editor benchmarks, instrumenting with PerfObserver, or optimizing the JS-side state management pipeline.
# Grida Canvas Editor — Performance Development Workflow and reasoning framework for performance work on the **TypeScript editor pipeline** — the reducer, Immer state management, React subscription layer, and headless benchmarks. > **Scope boundary:** This skill covers the **JS/TS editor** pipeline > (`editor/grida-canvas/`). For the **Rust rendering engine** (the > `grida` crate), use the engine repo's render-perf skill: > https://github.com/gridaco/nothing/blob/main/.agents/skills/render-perf/SKILL.md. > The two pipelines are connected — a dispatch in JS may trigger a WASM > re-render — but they are profiled with different tools, in different repos. > **Maintaining this document:** If you notice a section that has gone > stale (e.g. a workflow step no longer matches the code, a discovery > query returns nothing, or a pitfall has been resolved), update this > `SKILL.md` as part of your current task. Keep it high-level — > reference patterns and categories rather than specific function names > or measured numbers, which change frequently. ## When to Use This Skill - Benchmarking or profiling editor reducer operations - Diagnosing slow interactions (drag, resize, color picker, opacity slider) - Investigating Immer overhead or state cloning costs - Instrumenting code with `PerfObserver` spans - Writing or running the editor bench - Optimizing queries, snap targets, hover resolution - Reducing React re-render cost from editor state subscriptions --- ## Pick your measurement tool Performance work starts with the right signal. The three tools below are complementary — do not skip the browser trace if the user offers one, do not open a browser for a reducer-only regression. | Tool | Use when | What it catches | What it misses | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------- | | **Browser trace (Chrome DevTools Performance)** | User provided a `.json.gz` trace, or the symptom is interaction-level (drag/nudge feels laggy). **This is the truth.** | Everything on the main thread: React, selector cost, paint, compositor, GC, WASM, reducer, Immer, RAF. | Not reproducible without the user's session | | **editor-bench** (automated) | Default for proactive investigation and A/B of a reducer / encode / WASM change. No user input needed. | `PerfObserver` span table, per-scene (1K / 10K). Deterministic and comparable across runs. | React render cost, DOM overlays, real RAF | | **Node CPU profile** | A bench span points at a hotspot but you need function-level detail inside it, or microtask timing. | Function self-time and call chains within a single Node process. | Browser-only code paths | ### Decision rules 1. **If the user provides a browser trace, start there.** It already has React, DevTools, and WASM on the same timeline — quote actual numbers from the trace rather than guessing. Parse it as JSON (see "Reading a browser trace" below). 2. **Otherwise run the bench first.** No user input needed — it covers every scenario at 1K and 10K and emits a `PerfObserver` span table that's directly comparable across runs. 3. **Once bench points at a span, drill with a Node CPU profile.** Use the `withCpuProfile()` helper in `_utils.ts`, or run node with `--cpu-prof`. Open the resulting `.cpuprofile` in Chrome DevTools (Performance → Load profile) for a function-level flame graph inside the span. ## How to Orient Yourself Before touching any code, build context by reading these sources in order: 1. **Read `editor/grida-canvas/__tests__/bench/README.md`** — benchmark catalog, run instructions, and the authoritative list of `PerfObserver` spans with a "when to trust the numbers" guide. 2. **Read `editor/grida-canvas/perf.ts`** — the `PerfObserver` API. Understand `start()`, `measure()`, `report()`, `dump()`. 3. **Skim `editor/grida-canvas/editor.ts`** — the `EditorDocumentStore` class, specifically the `dispatch()` method. This is the single entry point for all state mutations. 4. **Skim `editor/grida-canvas/reducers/index.ts`** — the root reducer that wraps everything in Immer `produceWithPatches`. 5. **Browse the bench files** (`perf-editor.test.ts`, `perf-per-node-sync.test.ts`) to see what operations are already measured and at what scale. ### Key discovery queries | What you need | How to find it | | ---------------------------- | ------------------------------------------------------------------------------------- | | All instrumented perf spans | `grep "__perf_" --include="*.ts"` in `editor/grida-canvas/` | | The dispatch entry point | Search for `dispatch(` in `editor/grida-canvas/editor.ts` | | Root reducer + Immer produce | Read the top-level `reducer()` function in `editor/grida-canvas/reducers/index.ts` | | Gesture transform hot path | Search for `self_update_gesture_transform` in `editor/grida-canvas/reducers/methods/` | | Document query helpers | Read `editor/grida-canvas/query/index.ts` | | React hook subscribers | `grep "useEditorState" --include="*.ts"` in `editor/grida-canvas-react/` | | Action type definitions | Read `editor/grida-canvas/action.ts` | | Existing benchmark files | `ls editor/grida-canvas/__tests__/bench/` | --- ## The Architecture (Performance-Relevant) ### Dispatch Pipeline Every user interaction flows through this pipeline: ``` User action (click, drag, keystroke) → dispatch(action, recording) → Immer produceWithPatches(state, draft => { ... }) → sub-reducers (document, event-target, surface) → tracked-Graph wrapper emits sync_links / delete_node ops → appendPatchOps(patches, buffer) lifts document.nodes patches into replace_node / delete_node ops → history.record(patches) → postDispatchHooks → emit(action, patches, opLog) → __wasm_on_document_change applies opLog 1:1 to WASM → React selectors + equality checks ``` The op-log produced during the recipe is the single WASM-sync channel. Structural edges come from the tracked-`Graph` wrapper; node-property changes are lifted from Immer patches via `appendPatchOps`. The subscriber applies ops directly (`Scene.replaceNode` / `Scene.deleteNode` / `Scene.syncLinks`) or falls back to a full re-encode only when the batch contains a `full_resync` op. ### Performance-Sensitive Operation Categories **Gesture-bound (hot loop)** — fires on every frame while the user drags a handle, slider, or object. These must complete within ~16ms (60fps budget) per frame: - Property sliders (color picker, opacity, font size) - Drag translate (moving nodes) - Resize / scale (corner handles) - Rotate **Discrete (single shot)** — fires once per user click or toggle: - Select, rename, visibility toggle - Delete, insert - Gesture start / end (snapshot cost) - Pointer hover / raycast ### Cost Scaling Costs scale **linearly** with total node count due to Immer proxy finalization walking the entire state tree on every dispatch — even when only a single property on one node changes. This is the fundamental scaling wall for the current architecture. Run the benchmarks to get current numbers. The `perf.report()` output shows exactly which spans dominate at any given scale. ### Bottleneck Categories Use `GRIDA_PERF=1` to identify which category applies: | Category | How to recognize | Where to look | | ------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | **Immer overhead** | `reducer.immer_produce` dominates; cost grows with node count regardless of what changed | Root reducer, consider structural sharing or targeted produce | | **O(N) tree queries** | Tree-traversal spans are hot in the breakdown | `query/index.ts` — check if lookups can use pre-built index maps | | **Deep clone / snapshot** | `snapshot` span dominates gesture start | `editor.i.ts` — consider storing only what the gesture needs | | **Compute-heavy reducer logic** | Specific spans (snap, hover, transform) dominate | The relevant `reducers/tools/` or `reducers/methods/` file | | **React re-render** | Not visible headless; visible in Chrome DevTools Profiler | Selector breadth, equality comparators, virtualization | --- ## The Benchmark System The single source of truth is `perf-editor.test.ts` in `editor/grida-canvas/__tests__/bench/`. It uses `Editor.mountHeadless()` with the **real WASM raster backend** — every dispatch runs through the same `__wasm_on_document_change` subscriber the browser installs. Spans under `dispatch.wasm.*` are end-to-end identical to the browser; spans under `reducer.*` are pure JS and track within ~10% of browser V8. ```sh # Default — runs every scenario at 1K synthetic + bench.grida (10K) scales GRIDA_PERF=1 pnpm vitest run editor/grida-canvas/__tests__/bench/perf-editor.test.ts # With CPU profile capture (delete scenarios) GRIDA_PERF=1 GRIDA_PERF_CPUPROFILE=1 pnpm vitest run \ editor/grida-canvas/__tests__/bench/perf-editor.test.ts # Large fixtures need more heap NODE_OPTIONS="--max-old-space-size=8192" GRIDA_PERF=1 \ pnpm vitest run editor/grida-canvas/__tests__/bench/perf-editor.test.ts ``` ### Which spans to read After a bench run, `perf.report()` prints a table per scene. The bench README has the authoritative catalog — read it there. Read the table in category order rather than by specific name: 1. **Total dispatch** — your ceiling per action. 2. **Reducer + Immer** — pure JS cost. Watch p95 on gesture scenarios (drag / resize per-frame) — median can be microseconds while p95 spikes into hundreds of ms as the tree grows. 3. **Document snapshot** — deep-clone at gesture boundaries; pays twice per gesture (start + end). 4. **WASM sync (full reload vs. op-apply)** — compare `dispatch.wasm.sync_document` (full reload) against `dispatch.wasm.op_apply` (per-op replay). If the full reload fires for every dispatch and op_apply rarely fires, too many actions are routing through the slow path — usually a missing observation channel that forces `full_resync` (e.g. a mutation under `document.*` outside `document.nodes[id]` that no tracked channel covers). 5. **Gesture / query compute** — snap targets, hover ray, tree traversal. Usually small but can dominate at high selection count. Prefer bench ratios over absolute numbers in commits and memory — absolutes shift with machine and node version; ratios stay meaningful. ### Adding a new benchmark Extend `BENCH_SCENARIOS` in `perf-editor.test.ts`. Use the `bench()` helper from `_utils.ts` for per-frame gestures and `runAndTime()` (or similar) for single-shot discrete actions: ```ts const result = await bench(() => { h.ed.doc.dispatch({ type: "...", ... } as Action, { recording: "silent" }); }, { iterations: 10 }); logBench("my operation", result); ``` If you need a one-off CPU profile of a specific operation, use `withCpuProfile()` from `_utils.ts` — it wraps the call in `node:inspector` and writes a `.cpuprofile` to `fixtures/local/perf/cpuprofile/` (gated by `GRIDA_PERF_CPUPROFILE=1`). --- ## PerfObserver (`perf.ts`) Opt-in instrumentation layer. Zero cost when disabled (returns a shared `NOOP` function). ### Enable ```sh GRIDA_PERF=1 # Node.js / headless tests NEXT_PUBLIC_GRIDA_PERF=1 # Browser / Next.js (.env.local) ``` Or programmatically: ```ts
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る