Picker Performance Architecture
Deep dive into the performance optimizations that enable 60fps rendering with 1000+ seats
Overview
The SeatSquirrel Picker is architected for extreme performance, handling 1000+ seats at a smooth 60fps. This document explains the five major optimizations that make this possible.
CRITICAL: These optimizations are fundamental to the Picker's performance. Do NOT compromise them when extending functionality.
The Five Pillars of Performance
1. Pre-Calculated Seat Positions
Problem: Calculating seat positions on every render is expensive.
Solution: Calculate positions once and cache them.
const seatPositions = useMemo(() => {
return layout.seats.map(seat => ({
id: seat.id,
x: calculateX(seat),
y: calculateY(seat),
// ... other properties
}));
}, [layout.seats, layout.rows]);Benefits:
- Positions calculated only when seat data changes
- NOT recalculated on zoom, pan, or hover
- Stage transform handles all visual transformations
- Eliminates 1000+ calculations per render
Performance Impact: ~85% reduction in render time
2. Zero-Interaction Shape Rendering
Problem: Konva's event system checks every shape for interactions.
Solution: Disable interaction on all rendered shapes.
<Circle
listening={false} // ← CRITICAL
perfectDrawEnabled={false}
// ... shape properties
/>Why This Works:
- Konva skips hit detection for non-listening shapes
- Dramatically reduces per-frame calculations
- All interactions handled at Stage level instead
Rules:
- ✅ ALL shapes must have
listening={false} - ✅ Use
perfectDrawEnabled={false}for faster rendering - ❌ NEVER add
onClick,onHover, etc. to individual shapes - ❌ NEVER use Konva Groups (they add transform overhead)
Performance Impact: ~60% reduction in hover/click latency
3. Direct Konva Manipulation
Problem: React re-renders are expensive for smooth interactions.
Solution: Bypass React for zoom/pan, update directly via Konva.
// Wheel event handler
const handleWheel = (e: KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault();
const stage = e.target.getStage();
if (!stage) return;
// Direct manipulation - NO React re-render
const newScale = calculateScale(e.evt.deltaY);
stage.scale({ x: newScale, y: newScale });
stage.batchDraw(); // Immediate visual update
// Sync to React state later (debounced)
syncToState(newScale);
};Benefits:
- Immediate visual feedback (no React reconciliation delay)
- Smooth 60fps interactions
- React state updated only after interaction completes
Performance Impact: Achieves native-feeling pan/zoom
4. Debounced State Syncing
Problem: Updating Zustand state on every mouse move is expensive.
Solution: Update state only after interactions complete.
const syncTimeout = useRef<NodeJS.Timeout>();
const handleZoomEnd = (newScale: number) => {
// Clear any pending sync
if (syncTimeout.current) {
clearTimeout(syncTimeout.current);
}
// Debounce: sync after 100ms of idle
syncTimeout.current = setTimeout(() => {
pickerStore.setZoom(newScale);
}, 100);
};Benefits:
- Prevents expensive store updates during smooth scrolling
- Reduces state updates from hundreds to one per interaction
- Avoids triggering unnecessary re-renders
Performance Impact: ~70% reduction in state update overhead
5. Stage-Level Hover Detection
Problem: Per-shape hover detection scales poorly with seat count.
Solution: Single hover handler on Stage, manual hit detection.
<Stage
onMouseMove={(e) => {
const stage = e.target.getStage();
if (!stage) return;
const pointerPos = stage.getPointerPosition();
if (!pointerPos) return;
// Manual hit detection - check ONLY seats
const hit = stage.getIntersection(pointerPos);
if (hit && hit.name() === 'seat') {
const seatId = hit.id();
setHoveredSeat(seatId);
} else {
setHoveredSeat(null);
}
}}
>Benefits:
- One hover check per mouse move (not per seat)
- Only checks seats (ignores decorative shapes)
- Avoids Konva's automatic hit detection overhead
Performance Impact: Constant O(1) hover detection vs O(n)
Additional Optimizations
Minimal Shape Complexity
Seats:
- Rendered as simple
<Circle>components - No custom shapes with many points
- Direct property access (no computed values)
Areas:
- Use native Konva shapes (Rect, Ellipse, RegularPolygon)
- Avoid complex path calculations
- Pre-computed polygon points
Visual Elements:
- Minimal decorative shapes
- No expensive filters or effects
- No shadows or complex strokes
Color Optimization
Problem: Runtime color processing is expensive.
Solution: Use direct hex values, pre-calculate opacity.
// ❌ BAD - calculated at render time
fill={`rgba(${hexToRgb(color)}, ${opacity})`}
// ✅ GOOD - direct values
fill={color}
opacity={0.8}Tooltip Rendering
Problem: Canvas-based tooltips require re-renders.
Solution: Render tooltip in React DOM, not Konva canvas.
{/* Tooltip rendered OUTSIDE Konva Stage */}
{hoveredSeat && (
<div className="absolute" style={{...}}>
{/* Seat info */}
</div>
)}Benefits:
- No canvas re-render on hover
- Can use CSS animations
- Better text rendering
Performance Benchmarks
| Metric | Target | Typical | Notes |
|---|---|---|---|
| Render Time | under 16ms | 8-12ms | 60fps requires under 16ms |
| Seat Count | 1000+ | 500-2000 | Smooth performance |
| Zoom FPS | 60fps | 60fps | No dropped frames |
| Pan FPS | 60fps | 60fps | No dropped frames |
| Hover Latency | under 50ms | 10-20ms | Instant tooltip |
| Memory Usage | under 100MB | 40-80MB | For 1000 seats |
What NOT to Do
❌ Critical Mistakes
These will destroy performance:
-
Adding event handlers to shapes
// ❌ NEVER DO THIS <Circle onClick={handleClick} /> -
Using Groups or complex nesting
// ❌ NEVER DO THIS <Group><Group><Circle /></Group></Group> -
Enabling perfect drawing
// ❌ NEVER DO THIS <Circle perfectDrawEnabled={true} /> -
Adding filters or effects
// ❌ NEVER DO THIS <Circle filters={[Blur]} shadowBlur={10} /> -
Processing colors at render time
// ❌ NEVER DO THIS fill={calculateColorFromCategory(seat.category)} -
Custom shapes with many points
// ❌ NEVER DO THIS <Line points={generateHundredsOfPoints()} />
Testing Performance
In Development
- Open Chrome DevTools
- Enable "Performance" tab
- Record interaction (zoom/pan)
- Check frame rate (should be 60fps)
- Look for render times under 16ms
Key Metrics
- Frame Rate: Should stay at 60fps during interactions
- Render Time: Should be under 16ms (shown in Performance tab)
- Memory: Should not grow continuously (check for leaks)
Load Testing
Test with increasingly complex layouts:
- Small: 100 seats
- Medium: 500 seats
- Large: 1000 seats
- Extreme: 2000+ seats
Performance should remain smooth through "Large" tier.
Future Optimizations
Potential future improvements:
- Virtualization: Render only visible seats
- Web Workers: Offload calculations to background thread
- Canvas Caching: Cache static elements
- LOD (Level of Detail): Simplify shapes when zoomed out
Conclusion
The Picker's performance architecture is carefully designed to handle large-scale seating layouts. Every optimization works together to achieve 60fps rendering:
- Pre-calculated positions eliminate redundant calculations
- Zero-interaction rendering skips expensive hit detection
- Direct Konva manipulation provides instant visual feedback
- Debounced state syncing prevents unnecessary re-renders
- Stage-level hover detection scales to any seat count
Remember: Maintain these optimizations when adding new features. Performance is a feature, not an afterthought.
Next Steps
- Review Picker Overview
- Learn about Designer Performance
- Explore the MinimalCanvas Component