Advertisement
Advertisement
Back to Blog
performance webgl browser engineering

Avoiding the Freeze: Optimizing Heavy Client-Side Video Exports

K
Kinetix Team
January 6, 2024

Building a video editor in the browser pushes the limits of what web applications can do. One of the biggest challenges we faced with Kinetix was handling “offline” exports (frame-by-frame rendering) without freezing the entire browser UI.

When a user requests a 4K export at 60 FPS, the browser has to render, capture, encoded, and package thousands of high-resolution frames. If we do this in a tight loop, the main thread gets blocked, making the “Stop” button unresponsive and the progress bar freeze.

Here is how we solved it using Backpressure Throttling and Hardware Detection.

The Problem: The “Death Spiral”

In our initial implementation, our export loop was too aggressive:

// Naive implementation
for (let i = 0; i < totalFrames; i++) {
    engine.seek(time);
    const bitmap = await createImageBitmap(canvas);
    worker.postMessage({ bitmap }); // Queue up thousands of frames instantly
}

This flooded the encoding worker with thousands of messages. The browser’s memory usage spiked, and the main thread became unresponsive processing the message overhead.

Solution 1: Backpressure & Throttling

We moved to a “pull-like” system where we respect the worker’s processing speed. We interpret “backpressure” by monitoring the queue size of the worker.

1. Worker Queue Reporting

The worker now reports its queue size back to the main thread periodically (throttled to 50ms to avoid message spam):

// mediabunny.worker.ts
let lastProgressUpdate = 0;
const sendProgress = () => {
    if (Date.now() - lastProgressUpdate > 50) {
        self.postMessage({ type: 'PROGRESS', queueSize: pendingFrames });
        lastProgressUpdate = Date.now();
    }
}

2. Main Thread Yielding

In the main loop, we pause if the queue gets too full, and we explicitly yield to the UI thread every few frames.

// Core.ts
const BATCH_SIZE = 2; 

for (let i = 0; i < totalFrames; i++) {
    // Backpressure: Wait if worker is overwhelmed
    while (queueSize > 5) {
        await new Promise(r => setTimeout(r, 20));
    }

    // ... Render & Capture ...

    // Yield to UI to keep "Cancel" button clickable
    if (i % BATCH_SIZE === 0) {
        await new Promise(r => setTimeout(r, 2));
    }
}

Reducing the batch size to 2 frames and adding a 2ms sleep made the UI silky smooth even during 4K renders.

Solution 2: Client-Side Hardware Detection

Not all devices can handle 4K@60fps. A low-end Chromebook will crash where a MacBook Pro flies. We implemented a HardwareDetector to sniff the user’s capabilities.

// utils/HardwareDetector.ts
export class HardwareDetector {
    static getHardwareInfo() {
        // Core Count
        const cores = navigator.hardwareConcurrency || 4;
        
        // GPU Heuristics via WebGL Debug Info
        const gl = document.createElement('canvas').getContext('webgl');
        const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
        const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
        
        // Calculate abstract score
        const score = calculateScore(cores, renderer); // Custom scoring logic
        
        return { 
            tier: score > 80 ? 'high' : (score < 40 ? 'low' : 'medium'), 
            renderer 
        };
    }
}

Smart Warnings

Instead of blocking users, we provide helpful warnings. If a ‘low tier’ device attempts a 60FPS or 4K export, we show a performance warning:

“Your device seems to have limited hardware acceleration. Exporting at 60 FPS or High Resolution might cause freezing. Try 30 FPS if issues occur.”

Conclusion

Client-side video generation is heavy. By respecting the main thread with micro-yielding and implementing proactive hardware detection, we transformed a “browser crash” experience into a reliable, professional workflow.


Start Building Today

Ready to define your flow?

Join thousands of developers and designers creating clear, editable visuals instantly from text.