Server-side video rendering is expensive, operationally painful, and doesn't scale well. You need GPU instances, media processing queues, temp storage, CDN delivery pipelines — a whole infrastructure just to clip a video. We hit every one of those walls building ClipCrafter, and eventually asked: what if the browser just did it?
The result is framewebworker — an open-source library that runs video export entirely in the browser using OffscreenCanvas, Web Workers, and ffmpeg.wasm. No server. No queue. No infra.
The Problem with Server-Side Video Rendering
Traditional video rendering pipelines: user selects clips → API queues job → worker spins up ffmpeg → uploads to S3 → returns signed URL. This works, but every concurrent render ties up server resources. Peak usage means queuing delays. Cold starts on serverless are brutal for long-running ffmpeg processes.
The browser is now powerful enough to do this locally. For most use cases — trimming, merging, exporting clips — it's a better fit.
Introducing framewebworker
npm install framewebworker
GitHub: https://github.com/nareshipme/framewebworker | npm: https://www.npmjs.com/package/framewebworker
Core API
exportClips(videoUrl, segments[])
import { exportClips } from 'framewebworker';
const results = await exportClips('https://example.com/video.mp4', [
{ start: 0, end: 10, label: 'intro' },
{ start: 45, end: 60, label: 'highlight' },
]);
results.forEach(({ blob, label, metrics }) => {
console.log(label + ': ' + metrics.totalMs + 'ms @ ' + metrics.framesPerSecond + 'fps');
});
mergeClips(clips[])
import { mergeClips } from 'framewebworker';
const merged = await mergeClips([
'https://example.com/clip-a.mp4',
'https://example.com/clip-b.mp4',
]);
console.log('Merged in', merged.metrics.totalMs, 'ms');
useExportClips() — React hook
import { useExportClips } from 'framewebworker';
function ExportButton({ videoUrl, segments }) {
const { exportClips, progress, results, isExporting } = useExportClips();
return (
<button onClick={() => exportClips(videoUrl, segments)} disabled={isExporting}>
{isExporting ? 'Exporting... ' + progress + '%' : 'Export Clips'}
</button>
);
}
Architecture
Workers are spawned based on navigator.hardwareConcurrency (capped at 4). Each segment dispatches to the next available worker — multi-clip exports run in parallel. Each worker uses OffscreenCanvas for frame extraction and ffmpeg.wasm for encoding.
RenderMetrics
interface RenderMetrics {
extractionMs: number; // Frame extraction time
encodingMs: number; // ffmpeg encoding time
totalMs: number; // End-to-end wall clock
framesPerSecond: number; // Effective fps
}
COOP/COEP Headers Required
ffmpeg.wasm needs SharedArrayBuffer, which requires cross-origin isolation:
// next.config.js
module.exports = {
async headers() {
return [{
source: '/(.*)',
headers: [
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
{ key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
],
}];
},
};
Note: these headers can break cross-origin iframes and some OAuth popups. Audit your third-party dependencies before shipping.
Bundle Size
ffmpeg.wasm is ~30MB but loaded lazily — only triggered when the user initiates their first export. Initial page load is completely unaffected. First-export cold start is ~2-3s. After that, rendering is fast and stays in memory for the session.
When to Use framewebworker
Good fit: Web apps where users clip/trim/merge short videos, products eliminating backend rendering infra, controlled deployments where you can set COOP/COEP headers.
Not ideal: Very long videos (>30min), environments where you can't control response headers, apps with heavy third-party embeds that break under cross-origin isolation.
Get Started
npm install framewebworker
- GitHub: https://github.com/nareshipme/framewebworker
- npm: https://www.npmjs.com/package/framewebworker
- Production deep-dive: How We Ditched Backend Rendering and Went Full Client-Side with framewebworker
PRs, issues, and stars welcome. Multi-track audio mixing and subtitle burn-in are next on the roadmap.
Top comments (0)