Measured/traced hot spots, in order of payoff:
1. Pen tool: triple full-geometry pass per pointermove
Every pointermove in pen mode sets 4-5 pieces of state → full panel re-render (ResultPanel.tsx:1230-1237 → :703-842). Per event, findPenSnapTarget (:342), findAlignmentGuides (:491) and findLengthSnap (:599) each call flattenCurves over every piece — three dense-polygon allocations per move. The render then re-runs flattenCurves per piece again (:1765) and re-renders every PieceOverlay (not memoized; fresh displayPolygon array and onSelect closure each render, :1766-1777), so Konva reconciles every node and redraws the full-res pattern layer. SAM projects with dozens of pieces × hundreds of vertices make pen hover visibly laggy.
Fix: memoize flattened polygons per piece (keyed on polygon/curvePoints), React.memo PieceOverlay, build the snap-candidate set once per render instead of per move, move the hover preview to its own Konva layer.
2. One <img> + decode per piece for the same sheet texture
PieceOverlay calls useImage(glassImageUrl) per instance (ResultPanel.tsx:73); use-image has no cache (verified — fresh document.createElement('img') + decode per hook). Sheets are multi-MB base64 data URLs. 40 pieces on one sheet → 40 image elements and 40 async decodes on every remount. Lift the image per sheet (or add a module-level Map<url, HTMLImageElement>).
3. Tutorial animations re-render the world
- Marching-ants:
setDashOffset in a rAF loop re-renders all of ResultPanel at 60fps to advance a dash on one Rect (ResultPanel.tsx:996-1007). Use Konva.Animation/direct node mutation or isolate the rect in a memoized child.
SpotlightPulse.tsx:27-51 polls getBoundingClientRect in rAF (forced layout per frame). ResizeObserver would do.
4. getTooltipAnchor is O(total vertices) per pan/zoom frame
ResultPanel.tsx:199-241 (called at :2210): spreads min/max over the selected piece's polygon and computes centroids of all pieces every frame while panning. Also Math.min(...xs) on dense SAM polygons risks the V8 ~65k argument limit — use a loop. Memoize. (This whole function is deleted anyway if #34's docked inspector lands.)
5. Bundle: 1.25 MB main chunk, no code-splitting
vite build reports index-*.js at 1,247 kB (355 kB gzip) — three.js, Konva, ORT backend glue and the tutorial all load up front. three + Lamp3DPreview/LampProfileDialog are an obvious import() split (lamp mode is optional); the tutorial is another. Also consider manualChunks for konva/react vendors.
6. Structural: base64 images inside React state and undo snapshots
Pattern/sheet images live as data-URL strings in project state (ResultPanel.tsx:863-878, readAsDataURL), so every undo snapshot and every debounced autosave JSON.stringify re-serializes multi-MB strings on the main thread — every 500 ms during a drag. See the persistence-architecture refactor issue for the fix (OPFS-backed blobs + IDs).
Measured/traced hot spots, in order of payoff:
1. Pen tool: triple full-geometry pass per pointermove
Every pointermove in pen mode sets 4-5 pieces of state → full panel re-render (
ResultPanel.tsx:1230-1237→:703-842). Per event,findPenSnapTarget(:342),findAlignmentGuides(:491) andfindLengthSnap(:599) each callflattenCurvesover every piece — three dense-polygon allocations per move. The render then re-runsflattenCurvesper piece again (:1765) and re-renders everyPieceOverlay(not memoized; freshdisplayPolygonarray andonSelectclosure each render,:1766-1777), so Konva reconciles every node and redraws the full-res pattern layer. SAM projects with dozens of pieces × hundreds of vertices make pen hover visibly laggy.Fix: memoize flattened polygons per piece (keyed on
polygon/curvePoints),React.memoPieceOverlay, build the snap-candidate set once per render instead of per move, move the hover preview to its own Konva layer.2. One
<img>+ decode per piece for the same sheet texturePieceOverlaycallsuseImage(glassImageUrl)per instance (ResultPanel.tsx:73);use-imagehas no cache (verified — freshdocument.createElement('img')+ decode per hook). Sheets are multi-MB base64 data URLs. 40 pieces on one sheet → 40 image elements and 40 async decodes on every remount. Lift the image per sheet (or add a module-levelMap<url, HTMLImageElement>).3. Tutorial animations re-render the world
setDashOffsetin a rAF loop re-renders all of ResultPanel at 60fps to advance a dash on oneRect(ResultPanel.tsx:996-1007). UseKonva.Animation/direct node mutation or isolate the rect in a memoized child.SpotlightPulse.tsx:27-51pollsgetBoundingClientRectin rAF (forced layout per frame).ResizeObserverwould do.4.
getTooltipAnchoris O(total vertices) per pan/zoom frameResultPanel.tsx:199-241(called at:2210): spreads min/max over the selected piece's polygon and computes centroids of all pieces every frame while panning. AlsoMath.min(...xs)on dense SAM polygons risks the V8 ~65k argument limit — use a loop. Memoize. (This whole function is deleted anyway if #34's docked inspector lands.)5. Bundle: 1.25 MB main chunk, no code-splitting
vite buildreportsindex-*.jsat 1,247 kB (355 kB gzip) — three.js, Konva, ORT backend glue and the tutorial all load up front.three+Lamp3DPreview/LampProfileDialogare an obviousimport()split (lamp mode is optional); the tutorial is another. Also considermanualChunksfor konva/react vendors.6. Structural: base64 images inside React state and undo snapshots
Pattern/sheet images live as data-URL strings in project state (
ResultPanel.tsx:863-878,readAsDataURL), so every undo snapshot and every debounced autosaveJSON.stringifyre-serializes multi-MB strings on the main thread — every 500 ms during a drag. See the persistence-architecture refactor issue for the fix (OPFS-backed blobs + IDs).