Performance Guide¶
Complete guide to profiling and optimizing FiberPath GUI performance.
Overview¶
FiberPath GUI is designed for responsiveness with lazy loading, memoization, and optimized renders. This guide covers profiling tools and optimization patterns.
Profiling Tools¶
React DevTools Profiler¶
Installation:
- Install React DevTools extension (Chrome/Firefox)
- Open DevTools → React tab → Profiler
Recording a Session:
- Click record button (red circle)
- Perform actions in GUI (e.g., add layers, update mandrel)
- Stop recording
- Analyze flamegraph
Reading Flamegraph:
- Width: Time spent rendering
- Color: Fast (green) vs slow (yellow/red)
- Tooltip: Component name, render duration
- Drill down: Click to see child components
Example Findings:
PlanForm (12ms)
├─ MandrelSection (3ms)
├─ TowSection (2ms)
└─ LayerManager (7ms)
└─ LayerItem (1ms × 7 layers)
Chrome Performance Tab¶
Recording:
- Open DevTools → Performance tab
- Click record
- Perform actions
- Stop recording
Analysis:
- Main thread: JavaScript execution, layout, paint
- Frames: Green = good (60fps), red = dropped frames
- Summary: Time breakdown (scripting, rendering, painting)
Look for:
- Long tasks (>50ms)
- Layout thrashing
- Excessive repaints
Vite Build Analyzer¶
Analyze Bundle Size:
npm run build
Generates dist/assets/*.js files with size reports.
Identify Large Dependencies:
npx vite-bundle-visualizer
Opens interactive visualization of bundle contents.
Optimization Patterns¶
1. Memoization¶
useMemo for Expensive Computations¶
import { useMemo } from 'react';
function LayerList() {
const layers = useProjectStore((s) => s.project.layers);
// ✅ Memoize expensive sort
const sortedLayers = useMemo(
() => [...layers].sort((a, b) => a.index - b.index),
[layers]
);
return <div>{sortedLayers.map(/* render */)}</div>;
}
When to use:
- Sorting/filtering large arrays
- Complex calculations
- Object transformations
When NOT to use:
- Simple array maps (no transformation)
- Cheap operations (<1ms)
React.memo for Component Memoization¶
import { memo } from 'react';
const LayerItem = memo(function LayerItem({ layer }: { layer: Layer }) {
return <div>{layer.windType}: {layer.terminal ? 'Terminal' : 'Non-terminal'}</div>;
});
Behavior: Re-renders only if props change (shallow comparison).
Use Cases:
- List items that rarely update
- Expensive child components
- Pure presentation components
Avoid for:
- Components with frequent updates
- Components with object/array props (use custom comparison)
Custom Comparison¶
const LayerItem = memo(
function LayerItem({ layer }: { layer: Layer }) {
return <div>...</div>;
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.layer.id === nextProps.layer.id &&
prevProps.layer.windType === nextProps.layer.windType;
}
);
2. Zustand Shallow Selectors¶
import { shallow } from "zustand/shallow";
// ❌ Bad: Re-renders on any state change
const state = useProjectStore();
// ✅ Good: Re-renders only when mandrel changes
const mandrel = useProjectStore((s) => s.project.mandrel, shallow);
// ✅ Better: Re-renders only when diameter changes
const diameter = useProjectStore((s) => s.project.mandrel.diameter);
Shallow Comparison:
- Compares object keys/values one level deep
- Prevents re-renders when object reference changes but content doesn't
Best Practice: Use primitive selectors when possible.
3. Virtualization (for Large Lists)¶
Library: react-window or react-virtual
import { FixedSizeList } from 'react-window';
function LargeLayerList({ layers }: { layers: Layer[] }) {
return (
<FixedSizeList
height={600}
itemCount={layers.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<LayerItem layer={layers[index]} />
</div>
)}
</FixedSizeList>
);
}
Use Case: Lists with 100+ items.
4. Debouncing¶
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage
function DiameterInput() {
const [diameter, setDiameter] = useState(150);
const debouncedDiameter = useDebounce(diameter, 300);
const updateMandrel = useProjectStore((s) => s.updateMandrel);
useEffect(() => {
updateMandrel({ diameter: debouncedDiameter });
}, [debouncedDiameter]);
return (
<input
type="number"
value={diameter}
onChange={(e) => setDiameter(Number(e.target.value))}
/>
);
}
Use Cases:
- Search inputs
- Slider controls
- Auto-save
5. Code Splitting¶
import { lazy, Suspense } from 'react';
// ✅ Lazy load heavy components
const PlotPanel = lazy(() => import('./components/PlotPanel'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<PlotPanel />
</Suspense>
);
}
Benefit: Reduces initial bundle size, faster startup.
6. Event Handler Optimization¶
// ❌ Bad: Creates new function on every render
function PlanForm() {
const updateMandrel = useProjectStore((s) => s.updateMandrel);
return (
<input onChange={(e) => updateMandrel({ diameter: Number(e.target.value) })} />
);
}
// ✅ Good: Stable function reference
import { useCallback } from 'react';
function PlanForm() {
const updateMandrel = useProjectStore((s) => s.updateMandrel);
const handleDiameterChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
updateMandrel({ diameter: Number(e.target.value) });
},
[updateMandrel]
);
return <input onChange={handleDiameterChange} />;
}
When to use: When handler is passed to memoized child component.
Common Performance Issues¶
Issue: Excessive Re-renders¶
Symptom: Component renders multiple times per user action.
Diagnosis:
import { useEffect } from "react";
function MyComponent() {
useEffect(() => {
console.log("MyComponent rendered");
});
// ...
}
Causes:
- Selecting entire store instead of specific values
- Creating objects/arrays in render
- Prop changes triggering cascade
Solution:
- Use shallow selectors
- Memoize derived data
- Use React.memo for expensive children
Issue: Slow List Rendering¶
Symptom: Adding layer takes >500ms.
Diagnosis: Profile in React DevTools, check LayerManager duration.
Solutions:
- Add
keyprop to list items (use stable IDs) - Memoize LayerItem component
- Virtualize if 100+ items
Issue: Large Bundle Size¶
Symptom: Initial load >5 seconds.
Diagnosis: Run npx vite-bundle-visualizer.
Solutions:
- Code split heavy components (PlotPanel)
- Tree-shake unused dependencies
- Use dynamic imports for CLI-heavy features
Issue: Memory Leaks¶
Symptom: Memory grows over time, app becomes sluggish.
Diagnosis: Chrome DevTools → Memory → Heap Snapshot.
Common Causes:
- Event listeners not cleaned up
- Timers not cleared
- Zustand subscriptions not unsubscribed
Solution:
useEffect(() => {
const unlisten = listen("stream-progress", handleProgress);
return () => {
unlisten(); // Cleanup
};
}, []);
Performance Budgets¶
Target Metrics¶
| Metric | Target | Critical |
|---|---|---|
| First Contentful Paint | <1s | <2s |
| Time to Interactive | <2s | <3s |
| Component Render | <16ms | <50ms |
| Store Update | <5ms | <16ms |
| Bundle Size (JS) | <500KB | <1MB |
| Memory Usage (idle) | <100MB | <200MB |
Measuring¶
// Render time
performance.mark("render-start");
// ...component render
performance.mark("render-end");
performance.measure("render", "render-start", "render-end");
const [measure] = performance.getEntriesByName("render");
console.log(`Render took ${measure.duration.toFixed(2)}ms`);
Optimizing Tauri Commands¶
Async All the Things¶
// ❌ Bad: Blocking UI
const result = await planWind(inputPath);
setResult(result);
// ✅ Good: Show loading state
setIsLoading(true);
const result = await planWind(inputPath);
setResult(result);
setIsLoading(false);
Debounce Preview Updates¶
const debouncedScale = useDebounce(scale, 300);
useEffect(() => {
if (gcodePath) {
plotPreview(gcodePath, debouncedScale).then(setPreview);
}
}, [gcodePath, debouncedScale]);
Prevents: Rapid fire CLI calls on slider drag.
Testing Performance¶
Automated Performance Tests¶
import { render } from '@testing-library/react';
import { performance } from 'perf_hooks';
it('should render LayerList in <50ms', () => {
const layers = Array.from({ length: 100 }, (_, i) => createLayer('hoop'));
const start = performance.now();
render(<LayerList layers={layers} />);
const end = performance.now();
expect(end - start).toBeLessThan(50);
});
Synthetic Benchmarks¶
describe("Store performance", () => {
it("should handle 1000 layer adds in <100ms", () => {
const store = useProjectStore.getState();
const start = performance.now();
for (let i = 0; i < 1000; i++) {
store.addLayer("hoop");
}
const end = performance.now();
expect(end - start).toBeLessThan(100);
});
});
Profiling Checklist¶
Before optimizing:
- [ ] Profile with React DevTools Profiler
- [ ] Identify slowest component (>50ms)
- [ ] Check if component re-renders unnecessarily
- [ ] Verify selector granularity (primitive vs object)
- [ ] Check for inline object/array creation
- [ ] Confirm keys on list items are stable
After optimizing:
- [ ] Re-profile to verify improvement
- [ ] Test edge cases (100+ layers, large files)
- [ ] Ensure no new bugs introduced
- [ ] Document optimization in code comments
Resources¶
Next Steps¶
- State Management - Optimizing store access
- Tech Stack - Understanding Vite optimizations
- Testing Guide - Performance testing patterns