Optimizing Performance
StickerNest combines React, Three.js, and iframes - each with unique performance considerations. This skill covers profiling and optimization techniques across all layers.
Performance Budget
| Metric |
Target |
Critical |
| First Contentful Paint |
< 1.5s |
< 3s |
| Time to Interactive |
< 3s |
< 5s |
| Frame Rate (60fps) |
> 55fps |
> 30fps |
| Frame Rate VR (72fps) |
> 68fps |
> 60fps |
| Bundle Size (main) |
< 500KB |
< 1MB |
| Memory (heap) |
< 200MB |
< 500MB |
React Performance
Identifying Slow Renders
// 1. React DevTools Profiler
// - Record a session
// - Look for long render times (> 16ms = frame drop)
// - Identify components rendering too often
// 2. Why Did You Render (development)
// npm install @welldone-software/why-did-you-render
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, { trackAllPureComponents: true });
// 3. Manual timing
console.time('[Render] MyComponent');
// ... render ...
console.timeEnd('[Render] MyComponent');
Preventing Unnecessary Re-renders
// 1. Memoize components
const MyComponent = React.memo(({ data }) => {
return <div>{data.name}</div>;
});
// 2. Memoize expensive computations
const processedData = useMemo(() => {
return expensiveProcessing(rawData);
}, [rawData]); // Only recalculate when rawData changes
// 3. Memoize callbacks
const handleClick = useCallback((id: string) => {
doSomething(id);
}, []); // Stable reference
// 4. Use Zustand selectors properly
// BAD - re-renders on ANY store change
const state = useCanvasStore();
// GOOD - only re-renders when widgets change
const widgets = useCanvasStore((s) => s.widgets);
// BETTER - shallow compare for objects
import { shallow } from 'zustand/shallow';
const { widgets, selection } = useCanvasStore(
(s) => ({ widgets: s.widgets, selection: s.selection }),
shallow
);
Virtualization for Long Lists
import { FixedSizeList } from 'react-window';
// Instead of rendering all items:
// widgets.map((widget) => <WidgetCard widget={widget} />)
// Use virtualization:
<FixedSizeList
height={600}
width={300}
itemCount={widgets.length}
itemSize={80}
>
{({ index, style }) => (
<div style={style}>
<WidgetCard widget={widgets[index]} />
</div>
)}
</FixedSizeList>
Lazy Loading Components
// Lazy load heavy components
const HeavyEditor = React.lazy(() => import('./HeavyEditor'));
// Use with Suspense
<Suspense fallback={<LoadingSpinner />}>
<HeavyEditor />
</Suspense>
// Route-level lazy loading
const CanvasPage = React.lazy(() => import('./pages/CanvasPage'));
Three.js / Spatial Performance
Frame Rate Monitoring
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
function FPSMonitor() {
const frames = useRef(0);
const lastTime = useRef(performance.now());
useFrame(() => {
frames.current++;
const now = performance.now();
if (now - lastTime.current >= 1000) {
console.log('[FPS]', frames.current);
frames.current = 0;
lastTime.current = now;
}
});
return null;
}
Geometry Optimization
// 1. Reuse geometries
const sharedGeometry = useMemo(() => new THREE.PlaneGeometry(1, 1), []);
// 2. Instanced meshes for many identical objects
import { Instances, Instance } from '@react-three/drei';
<Instances limit={1000} geometry={sharedGeometry} material={sharedMaterial}>
{positions.map((pos, i) => (
<Instance key={i} position={pos} />
))}
</Instances>
// 3. Merge static geometries
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils';
Material Optimization
// 1. Reuse materials
const sharedMaterial = useMemo(
() => new THREE.MeshStandardMaterial({ color: '#1e1b4b' }),
[]
);
// 2. Use simpler materials when possible
<meshBasicMaterial /> // Fastest - no lighting
<meshLambertMaterial /> // Fast - simple lighting
<meshStandardMaterial /> // Medium - PBR
<meshPhysicalMaterial /> // Slow - full PBR
// 3. Disable features you don't need
<meshStandardMaterial
flatShading // Skip normal interpolation
dithering={false} // Skip dithering
/>
Culling and LOD
// 1. Frustum culling (enabled by default)
<mesh frustumCulled={true}>
// 2. Level of Detail
import { LOD } from 'three';
const lod = new LOD();
lod.addLevel(highDetailMesh, 0); // Use when close
lod.addLevel(mediumDetailMesh, 50); // Use at 50 units
lod.addLevel(lowDetailMesh, 100); // Use at 100 units
// 3. Distance-based rendering
const distance = camera.position.distanceTo(object.position);
if (distance > 100) return null; // Don't render far objects
VR-Specific Optimization
// 1. Target 72fps minimum (Quest), 90fps (PC VR)
// Frame budget: ~14ms for 72fps, ~11ms for 90fps
// 2. Reduce draw calls
// - Batch similar materials
// - Use instancing
// - Merge static geometry
// 3. Foveated rendering (if supported)
// The center of view renders at full res, edges at lower res
// 4. Reduce Html components
// Each <Html> creates DOM overlay - expensive in XR
// Use <Text> from drei instead for labels
Bundle Size Optimization
Analyzing Bundle
# Generate bundle analysis
npm run build -- --analyze
# Or use source-map-explorer
npx source-map-explorer dist/assets/*.js
Code Splitting
// 1. Route-based splitting (automatic with lazy)
const routes = [
{ path: '/', element: lazy(() => import('./pages/Home')) },
{ path: '/canvas', element: lazy(() => import('./pages/Canvas')) },
];
// 2. Feature-based splitting
const loadAIFeatures = () => import('./features/ai');
// 3. Vendor chunking (vite.config.ts)
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
'vendor-three': ['three', '@react-three/fiber', '@react-three/drei'],
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
}
Tree Shaking
// BAD - imports entire library
import _ from 'lodash';
_.debounce(fn, 100);
// GOOD - imports only what's needed
import debounce from 'lodash/debounce';
debounce(fn, 100);
// Also check for side-effect imports
import 'heavy-library'; // Might not tree-shake
Memory Optimization
Detecting Memory Leaks
// 1. Chrome DevTools Memory tab
// - Take heap snapshots before/after operations
// - Compare to find retained objects
// 2. Common leak sources:
// - Event listeners not removed
// - setInterval/setTimeout not cleared
// - Subscriptions not unsubscribed
// - Closures holding references
// 3. Cleanup pattern
useEffect(() => {
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);
const interval = setInterval(() => { /* ... */ }, 1000);
const subscription = store.subscribe(() => { /* ... */ });
return () => {
window.removeEventListener('resize', handler);
clearInterval(interval);
subscription(); // Unsubscribe
};
}, []);
Three.js Memory Management
// Dispose geometries and materials when done
useEffect(() => {
return () => {
geometry.dispose();
material.dispose();
texture?.dispose();
};
}, []);
// For dynamic scenes, track and dispose
const disposables = useRef<THREE.Object3D[]>([]);
function addObject(obj: THREE.Object3D) {
scene.add(obj);
disposables.current.push(obj);
}
function cleanup() {
disposables.current.forEach((obj) => {
scene.remove(obj);
if (obj instanceof THREE.Mesh) {
obj.geometry.dispose();
(obj.material as THREE.Material).dispose();
}
});
disposables.current = [];
}
Performance Checklist
Before Shipping
For Components
For Three.js
For Data