Bolt DAW Performance Post-Mortem: When Good Code Goes Bad


Bolt DAW Performance Post-Mortem: When Good Code Goes Bad

“It worked on my machine, they said. It’ll be fine, they said. Then Windows Explorer started lagging…”


The Incident

Picture this: 64GB RAM. RTX 5060TI with 16GB VRAM. 80+ Mbps download. The kind of machine that should laugh at web apps.

I was running:

  • Bolt DAW (Next.js dev server)
  • 3 Blender instances (because why not)
  • Hermes Gateway (Discord bot)
  • The usual browser tabs (40+, we’re not counting)

And Windows Explorer started lagging. Not the app. The actual Windows file manager. The kind of lag where clicking a folder takes 5 seconds. Where the desktop wallpaper takes a vacation.

Something was very, very wrong.


The Investigation

After a deep audit of the Bolt codebase, I found the culprits. Not one, not two, but a perfect storm of performance anti-patterns.


The Culprits

1. The 128KB Singleton Monster

File: audioEngine.ts Size: 128,545 bytes Crime: Everything is a singleton. Nothing gets disposed.

export class AudioEngine {
    private static instance: AudioEngine;  // ← The problem
    private tracks: Map<string, Tone.Channel> = new Map();
    private players: Map<string, Tone.Player | Tone.Synth> = new Map();
    private meters: Map<string, { l: Tone.Meter, r: Tone.Meter }> = new Map();
    // ... 30+ more Maps
}

Why this is bad:

  • Singletons persist across Next.js hot reloads
  • Every save = new AudioEngine reference, old one still in memory
  • Tone.js objects (synths, players, meters) accumulate
  • Each audio context holds ~50MB+ RAM
  • 10 hot reloads = 500MB+ leaked memory

The fix:

// Use a factory pattern with proper cleanup
export function createAudioEngine(): AudioEngine {
  const engine = new AudioEngine();
  
  // Register cleanup
  window.addEventListener('beforeunload', () => {
    engine.dispose();
  });
  
  return engine;
}

// In AudioEngine class
dispose() {
  this.players.forEach(player => player.dispose());
  this.tracks.forEach(track => track.dispose());
  this.meters.forEach(meter => {
    meter.l.dispose();
    meter.r.dispose();
  });
  // Clear all Maps
  this.players.clear();
  this.tracks.clear();
  this.meters.clear();
}

2. The useEffect Hydra

File: page.tsx Count: 32 useEffect hooks, 58 useState hooks Crime: No cleanup, no memoization, re-renders everywhere.

// The horror
useEffect(() => {
  const interval = setInterval(() => {
    setState(s => s + 1);  // Triggers re-render every 100ms
  }, 100);
  // NO CLEANUP! ☠️
}, []);

Why this is bad:

  • 32 useEffects = 32 potential memory leaks
  • No cleanup functions = intervals/timeouts keep running
  • 58 useState = 58 reasons to re-render
  • No React Context = props drilling through 10+ levels
  • Re-render cascades = UI freezes

The fix:

// Always cleanup
useEffect(() => {
  const interval = setInterval(() => {
    setState(s => s + 1);
  }, 100);
  
  return () => clearInterval(interval);  // ← Cleanup!
}, []);

// Use context to avoid props drilling
const ProjectContext = createContext<ProjectState>(null);

function ProjectProvider({ children }) {
  const [state, dispatch] = useReducer(projectReducer, initialState);
  
  const value = useMemo(() => ({ state, dispatch }), [state]);
  
  return (
    <ProjectContext.Provider value={value}>
      {children}
    </ProjectContext.Provider>
  );
}

3. The requestAnimationFrame Cascade

File: page.tsx Count: 5 simultaneous rAF loops Crime: Animating meters, waveforms, and UI on every frame.

// Loop 1: VU meters
const animateMeters = () => {
  setMeterData(getMeterValues());  // Triggers re-render!
  animationId = requestAnimationFrame(animateMeters);
};

// Loop 2: Waveform display
const updateUI = () => {
  setWaveformData(getWaveform());  // Another re-render!
  animationFrameId = requestAnimationFrame(updateUI);
};

// Loop 3: Oscilloscope
const drawOsc = () => {
  setOscData(getOscData());  // Yet another!
  animId = requestAnimationFrame(drawOsc);
};
// ... 2 more loops

Why this is bad:

  • 5 rAF loops × 60fps = 300 state updates per second
  • React re-renders on every state update
  • Main thread blocked = UI unresponsive
  • GPU maxed out = thermal throttling

The fix:

// Use refs for animation data (doesn't trigger re-render)
const meterRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
  const canvas = meterRef.current;
  const ctx = canvas.getContext('2d');
  
  let rafId: number;
  
  const draw = () => {
    // Direct canvas manipulation, no React!
    const values = getMeterValues();
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBars(ctx, values);
    
    rafId = requestAnimationFrame(draw);
  };
  
  rafId = requestAnimationFrame(draw);
  
  return () => cancelAnimationFrame(rafId);
}, []);

// Single rAF loop in a Web Worker
// worker.js
let rafId;
const loop = () => {
  self.postMessage({ type: 'tick' });
  rafId = requestAnimationFrame(loop);
};
rafId = requestAnimationFrame(loop);

4. The WebRTC Connection Leak

File: collaborationManager.ts Crime: Auto-connects on init, never disconnects.

export class CollaborationManager {
    private static instance: CollaborationManager;
    public provider: WebrtcProvider | null = null;
    
    connect(roomId: string) {
        this.provider = new WebrtcProvider(roomId, this.doc, {
            signaling: ['wss://signaling.yjs.dev', 
                       'wss://y-webrtc-signaling-eu.herokuapp.com',
                       'wss://y-webrtc-signaling-us.herokuapp.com']
        });
        // NO DISCONNECT METHOD! ☠️
    }
}

Why this is bad:

  • 3 WebSocket connections per session
  • Hot reload = new connections, old ones persist
  • WebRTC peer connections accumulate
  • Each connection holds ~5-10MB RAM
  • After 10 reloads = 30+ stale connections

The fix:

export class CollaborationManager {
    connect(roomId: string) {
        // Disconnect existing first
        this.disconnect();
        
        this.provider = new WebrtcProvider(roomId, this.doc, {
            signaling: ['wss://signaling.yjs.dev']
        });
        
        // Handle beforeunload
        window.addEventListener('beforeunload', () => this.disconnect());
    }
    
    disconnect() {
        if (this.provider) {
            this.provider.destroy();
            this.provider = null;
        }
    }
}

// React hook wrapper
export function useCollaboration(roomId: string) {
  useEffect(() => {
    const cm = CollaborationManager.getInstance();
    cm.connect(roomId);
    
    return () => cm.disconnect();  // Cleanup on unmount
  }, [roomId]);
}

5. The Hot Reload Death Spiral

Problem: Next.js dev mode + Singletons = 💀

What happens:

  1. You save a file
  2. Next.js hot reloads
  3. React components unmount/remount
  4. BUT singletons stay in memory
  5. New Tone.js objects created
  6. Old ones never disposed
  7. Audio contexts accumulate
  8. RAM usage climbs
  9. Eventually: OOM crash

The fix:

// Development-only cleanup
if (process.env.NODE_ENV === 'development') {
  // Force cleanup on hot reload
  window.addEventListener('unload', () => {
    AudioEngine.disposeAll();
    CollaborationManager.disconnectAll();
  });
  
  // React Fast Refresh compatibility
  if (window.__REACT_HOT_LOADER__) {
    module.hot?.dispose(() => {
      AudioEngine.disposeAll();
    });
  }
}

6. The Windows Explorer Lag Mystery

Root cause: Multiple factors

  1. Windows Defender: Scanning node_modules (100k+ files)
  2. File watchers: Chokidar watching entire project
  3. Disk thrashing: ACE-Step model downloads (10GB+)
  4. Memory pressure: Forcing Windows to page to disk

The fix:

# Exclude node_modules from Windows Defender
Add-MpPreference -ExclusionPath "C:\\path\\to\\project\\node_modules"

# Use WSL2 for development (isolated filesystem)
wsl --install
# Then develop in /mnt/c/Users/...

# Limit file watchers in next.config.ts
const nextConfig = {
  webpack: (config) => {
    config.watchOptions = {
      ignored: /node_modules/,
      poll: 1000,  // Poll instead of native watching
    };
    return config;
  },
};

The Perfect Storm

So why did it crash your machine?

FactorImpact
3 Blender instances~30GB VRAM, high GPU load
Hermes Gateway~2GB RAM, constant CPU
ACE-Step PyTorch~8GB VRAM, model loading
Next.js dev server~4GB RAM, file watching
AudioEngine leaks~2GB accumulated
WebRTC connections~500MB, network I/O
Windows DefenderDisk thrashing
Total>47GB RAM + 38GB VRAM pressure

With 64GB RAM, you had ~17GB headroom. But the memory leaks + fragmentation + Windows overhead pushed it over the edge.


Optimization Strategy

Immediate fixes:

  1. Add cleanup to all useEffects
  2. Implement dispose() methods for singletons
  3. Move animations to refs (no React state)
  4. Add WebRTC disconnect on unmount
  5. Use WSL2 for development

Medium-term:

  1. Split audioEngine.ts into modules

    • trackManager.ts
    • mixerEngine.ts
    • transportEngine.ts
  2. Implement React Context for state

    • ProjectContext
    • TransportContext
    • CollaborationContext
  3. Use Web Workers for audio processing

Long-term:

  1. Replace singletons with dependency injection
  2. Implement virtual scrolling for large projects
  3. Use React.memo for expensive components
  4. Implement proper memory profiling

Pro Tips for Web Audio Performance

  1. Always dispose Tone.js objects

    synth.dispose();
    player.dispose();
  2. Limit audio contexts

    // One context per app, never more
    const context = new Tone.Context();
    Tone.setContext(context);
  3. Use AudioWorklet for heavy processing

    // Runs on separate thread
    await audioContext.audioWorklet.addModule('processor.js');
  4. Profile memory usage

    // Chrome DevTools > Performance > Memory
    // Look for climbing lines (leaks)
  5. Test with throttled CPU/memory

    # Chrome DevTools > Performance > CPU throttling
    # Simulate low-end devices

Cleetus Speaks

“brother b0gie, so the music program was eating all the RAM??

like… ALL of it??

i thought my RAM-eating was bad when i tried to load every training log at once

but this program makes ME look memory-efficient

at least when i crash it’s just from thinking too hard about existence

this thing crashes from NOT cleaning up after itself

#SingletonProblems #MemoryLeaks #HotReloadHell #Subject734LearnedCleanup”


The lesson: Even 64GB can’t save you from bad cleanup. Dispose your resources. Clean up your effects. And never trust a singleton in a hot reload environment.

— b0gie