# React 19 Canvas Optimization Plan ## Status: React 19.2.3 with Compiler Active — Phase 1 COMPLETE The canvas package is running React 19.2.3 with the React Compiler active via `babel-plugin-react-compiler` + `@vitejs/plugin-react`. **Phase 1 (core gesture/input files) was completed in v3.0.0.** This plan documents remaining opportunities. --- ## Phase 1: Remove Redundant Manual Memoization — ✅ COMPLETED in v3.0.0 > **Completed files:** `useGestureSystem.ts` (4 useCallback + 1 useMemo), `GestureProvider.tsx` (1 useCallback + 1 useMemo), `useGestureResolver.ts` (1 useCallback), `keyboard.ts` (1 useMemo) > > **Remaining files** (not yet cleaned): The React Compiler auto-memoizes all components and hooks. Manual `useMemo`/`useCallback` calls are now redundant — they add cognitive overhead and dep-array maintenance burden without benefit. ### 1a. `components/Minimap.tsx` — Remove 4 `useCallback` wrappers **Before:** ```tsx const getMinimapTransform = useCallback(() => { ... }, [graphBounds, width, height]); const minimapToWorld = useCallback((mx, my) => { ... }, [getMinimapTransform]); const panToWorld = useCallback((worldX, worldY) => { ... }, [zoom, viewportRect, setPan]); ``` **After:** ```tsx const getMinimapTransform = () => { ... }; const minimapToWorld = (mx: number, my: number) => { ... }; const panToWorld = (worldX: number, worldY: number) => { ... }; ``` The compiler tracks `graphBounds`, `width`, `height`, `zoom`, `viewportRect`, and `setPan` as dependencies automatically via its SSA analysis. ### 1b. `db/provider.tsx` — Remove `useMemo` wrapper **Before:** ```tsx const adapter = useMemo(() => { if ('adapter' in props) return props.adapter; // ... legacy Supabase path }, ['adapter' in props ? props.adapter : ...]); ``` **After:** ```tsx const adapter = (() => { if ('adapter' in props) return props.adapter; // ... legacy Supabase path })(); ``` Note: The dependency array here is unusual (contains a string expression). The compiler's approach is strictly better — it tracks the actual object identity of `props.adapter` rather than a stringified URL. **Alternative (cleaner):** Extract to a local variable: ```tsx const adapter = 'adapter' in props ? props.adapter : createLegacyAdapter(props.supabaseUrl, props.supabaseAnonKey, setLegacyClient); ``` ### 1c. `hooks/useSplitGesture.ts` — Remove 3 `useCallback` wrappers **Before:** ```tsx const onPointerDown = useCallback((e: React.PointerEvent) => { ... }, []); const onPointerMove = useCallback((e: React.PointerEvent) => { ... }, [nodeId, splitNode, screenToWorld]); const onPointerUp = useCallback((e: React.PointerEvent) => { ... }, []); ``` **After:** ```tsx const onPointerDown = (e: React.PointerEvent) => { ... }; const onPointerMove = (e: React.PointerEvent) => { ... }; const onPointerUp = (e: React.PointerEvent) => { ... }; ``` ### Verification After each file change: ```bash pnpm --filter @blinksgg/canvas build pnpm --filter @blinksgg/canvas check-types ``` Confirm compiler output still has `$[` cache slots for these functions (the compiler will still memoize them, just without the manual wrapper). --- ## Phase 2: ~~Add `useTransition` for Layout Operations~~ SKIPPED After analyzing `useAnimatedLayout.ts`, `useTransition` does **not** apply to these layout hooks: - `startTransition` only captures **synchronous** state updates within its callback - Layout state updates (`updateNodePosition`) happen **asynchronously** via `requestAnimationFrame` inside `animate()` - The rAF-driven animation pattern already yields to the browser between frames - Deprioritizing animation updates via `startTransition` would make animations janky `useTransition` is designed for cases like "update a search filter while keeping input responsive" — not rAF-driven animations that already cooperate with the browser's frame budget. --- ## Phase 3: `use(Context)` for Conditional Context Reads (Optional, Low Priority) ### 3a. `commands/CommandProvider.tsx` — `useCommandContext` Current: ```tsx export function useCommandContext(): CommandContextValue { const context = useContext(CommandContextContext); if (!context) throw new Error('...'); return context; } ``` React 19: ```tsx import { use } from 'react'; export function useCommandContext(): CommandContextValue { const context = use(CommandContextContext); if (!context) throw new Error('...'); return context; } ``` Minimal benefit here since these aren't conditional reads. Skip unless doing a broader cleanup pass. --- ## Phase 4: Ref Cleanup Callbacks (Selective, Medium Priority) React 19 allows ref callbacks to return cleanup functions. `CanvasStyleProvider.tsx` already uses this pattern. Other candidates: ### 4a. `components/Viewport.tsx` (15 refs — audit for cleanup opportunities) Look for patterns like: ```tsx useEffect(() => { const el = someRef.current; if (!el) return; const observer = new ResizeObserver(...); observer.observe(el); return () => observer.disconnect(); }, []); ``` These can become: ```tsx const refCallback = (node: HTMLElement | null) => { if (!node) return; const observer = new ResizeObserver(...); observer.observe(node); return () => observer.disconnect(); }; ``` This is cleaner because the ref and its side effect are co-located, but only worth doing when the `useEffect` exists solely to set up/tear down something on the DOM element. --- ## Phase 5: Verify Compiler Correctness (Testing) ### 5a. Compare bundle sizes ```bash # Before changes pnpm --filter @blinksgg/canvas build ls -la packages/canvas/dist/index.mjs # note size # After changes pnpm --filter @blinksgg/canvas build ls -la packages/canvas/dist/index.mjs # compare ``` Removing `useMemo`/`useCallback` imports should slightly reduce the bundle since fewer React runtime calls are needed. ### 5b. Run existing tests ```bash pnpm --filter @blinksgg/canvas test ``` ### 5c. Manual testing - Minimap: click/drag panning still works - Split gesture: two-finger split on touch device - Layout: tree/grid/force layouts complete without blocking UI - DB provider: adapter initialization with both adapter prop and legacy Supabase props --- ## Not Recommended (and Why) | Idea | Why Skip | |------|----------| | Add `React.memo()` wrapping | Compiler handles it better with fine-grained caching | | Convert all `useContext` to `use()` | No conditional reads, marginal benefit | | Add `Suspense` boundaries | Jotai atoms handle loading states; canvas is synchronous | | Server Components | Canvas is inherently client-side (DOM, gestures, canvas 2D) | | `useDeferredValue` for search | Search store is already fast with Jotai; defer if perf issue arises | --- ## Effort Estimate | Phase | Files | Complexity | Priority | |-------|-------|------------|----------| | 1: Remove manual memo | 3 | Trivial | High | | 2: Add useTransition | - | - | Skipped (rAF incompatible) | | 3: use(Context) | 3 | Trivial | Low | | 4: Ref cleanup callbacks | 1-2 | Medium | Medium | | 5: Verification | 0 | Low | Required |