Skip to content

Latest commit

 

History

History
166 lines (115 loc) · 4.13 KB

File metadata and controls

166 lines (115 loc) · 4.13 KB

Performance

Keeping your app fast as it grows.


Code Splitting (Most Important)

Vite handles code splitting automatically. Every React.lazy(() => import(...)) in your route manifest becomes its own chunk -- no configuration needed. You add a feature, Vite creates a chunk. That's it.

// This single line is all it takes. Vite splits the chunk automatically.
{ id: 'my-feature', path: '/my-feature', component: lazy(() => import('../pages/features/MyFeaturePage')) },

The result:

  • The main bundle stays small (~50KB: framework + router + manifest) regardless of how many features you add
  • Each feature loads ~10-20KB on demand, only when the user navigates to it
  • At 200 features, the initial load is the same as at 2

The two-tier registry pattern takes this further by lazy-loading feature metadata (icons, descriptions, categories) separately from the routes themselves. See Bundle Discipline for the full strategy.


Debouncing User Input

For features that process input in real-time (text transforms, search, validation):

import { useDebounce } from '../hooks/useDebounce';

function SearchFeature() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      performSearch(debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Why 300ms: Fast enough to feel responsive, slow enough to avoid unnecessary work. Adjust per feature -- search may want 150ms, expensive computations may want 500ms.


Memoization

For expensive computations that depend on stable inputs:

const result = useMemo(() => {
  return expensiveCalculation(input);
}, [input]);

When to use:

  • Complex data transformations
  • Filtering/sorting large lists
  • Regex operations on large text

When NOT to use:

  • Simple lookups or string operations
  • Values that change on every render
  • Premature optimization (measure first)

Web Workers

For operations that take >50ms and would block the UI thread:

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

// Component
const worker = useMemo(() => new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }), []);

useEffect(() => {
  worker.postMessage(inputData);
  worker.onmessage = (e) => setResult(e.data);
}, [inputData, worker]);

Use cases: Image processing, large file parsing, complex math, data compression.


Image and File Handling

Object URL Cleanup

When creating URLs for file previews, always clean up to prevent memory leaks:

import { useObjectURL } from '../hooks/useObjectURL';

function ImagePreview({ file }) {
  const [url, setBlob] = useObjectURL();

  useEffect(() => {
    setBlob(file);
  }, [file, setBlob]);

  return url ? <img src={url} alt="Preview" /> : null;
}

The useObjectURL hook automatically revokes old URLs when creating new ones and on unmount.

Canvas High-DPI

For canvas-based features, use the syncCanvas utility to handle high-DPI displays:

import { syncCanvas } from '../utils/canvasHiDPI';

useEffect(() => {
  syncCanvas(canvasRef.current, width, height);
  const ctx = canvasRef.current.getContext('2d');
  // Draw at logical coordinates -- the utility handles scaling
}, [width, height]);

Without this, canvas content appears blurry on Retina/high-DPI displays.


React-Specific Optimizations

Avoid Unnecessary Re-renders

// Stable callbacks with useCallback
const handleClick = useCallback(() => {
  setCount(c => c + 1);
}, []);

// Stable objects with useMemo
const config = useMemo(() => ({
  format: 'json',
  indent: 2,
}), []);

Lazy-Load Heavy Components

For components that use large libraries (Three.js, CodeMirror, etc.):

const HeavyEditor = lazy(() => import('./components/HeavyEditor'));

// Only loads when isEditing becomes true
{isEditing && (
  <Suspense fallback={<LoadingSpinner />}>
    <HeavyEditor />
  </Suspense>
)}