VideoFlowcodeGitHubStudioTry itCoreRenderersReact Video EditorPlaygroundExamplesDocscodeGitHubStudioTry it
Getting started
InstallationQuick startCore conceptsYour first video
Builder
Builder APITime formatsParallel & wait
Layers
TextImageVideoAudioCaptionsShapeGroups
Animation
Animate & keyframesEasing functionsTransitionsEffects
Renderers
Browser rendererServer rendererDOM preview
React Video Editor
QuickstartThemingUploadsCustom panelsHooks & commandsKeyboard shortcuts
API reference
Overview@videoflow/core@videoflow/renderer-browser@videoflow/renderer-server@videoflow/renderer-dom@videoflow/react-video-editor

Rendering on a server

@videoflow/renderer-server runs in Node. It launches headless Chromium via Playwright, loads the same browser renderer the browser package uses, and exports an MP4 — by default entirely inside the browser (WebCodecs + MediaBunny mux), with no ffmpeg dependency. An alternative ffmpeg pipeline is also available for hosts that need custom encoder flags.

Install

npm install @videoflow/core @videoflow/renderer-server
npx playwright install chromium

Node 18+ is required. ffmpeg is optional — only install it system-wide if you plan to pass { ffmpeg: true }.

To a file

import ServerRenderer from '@videoflow/renderer-server';
import VideoFlow from '@videoflow/core';

const $ = new VideoFlow({ width: 1920, height: 1080, fps: 30 });
// ... build flow ...
const json = await $.compile();

await ServerRenderer.render(json, {
  outputType: 'file',
  output: './out.mp4',
  onProgress: (p) => console.log(`${Math.round(p * 100)}%`),
});

To a buffer

const buffer = await ServerRenderer.render(json, { outputType: 'buffer' });
// e.g. upload to S3 straight from memory
await s3.send(new PutObjectCommand({ Bucket, Key, Body: buffer, ContentType: 'video/mp4' }));

Options

OptionDefaultNotes
ffmpegfalseWhen true, switches to the alternative per-frame screenshot → ffmpeg pipeline. Requires ffmpeg 4.4+ on PATH.
outputType'buffer'`'file'
outputPath when outputType: 'file'.
signalAbortSignal for cancellation.
onProgress(p: 0..1) => void.
verbosefalseStream stage logs to the console.
videoQuality'veryHigh'`'medium'
videoBitrateExplicit bits per second (e.g. 16_000_000). Overrides videoQuality.
elementCaptureautoLeave unset to let the renderer decide. false forces the SVG rasterizer, true forces capture on. See Rendering performance.
elementCaptureScale1Supersample factor for element capture, max 4. Costs scale quadratically.
layerTypesExternal layer types to register — see Custom layer types.

Rendering performance

Two things dominate a render, and both are handled for you.

Frames are composited with Chromium's "HTML in canvas" (drawElementImage) when the browser supports it, instead of serializing every layer into an SVG <foreignObject>. That removes the font re-embedding, canvas re-encoding and XML serialization the SVG path needs. Measured end-to-end on the bundled examples:

exampleSVG rasterizerelement capture
effects showcase970 ms/frame480 ms/frame
nested groups218 ms/frame140 ms/frame

It needs Chrome 149+, and the renderer drives your system Chrome — so keeping Chrome current is all it takes. Verbose renders say which path was taken, and name the version when it is too old. Set VIDEOFLOW_CHROME_PATH to point at a specific binary.

Where a project animates scale or position more slowly than the browser's paint grid, the renderer automatically falls back to the SVG rasterizer for that project, because its scale latch keeps such motion smooth. You do not normally need to touch any of this.

Video layers decode through WebCodecs, adapting to the access pattern rather than seeking per frame. A seek re-decodes from the preceding keyframe, so seeking frame by frame costs O(frames × GOP) — on a 1080p clip with a stock 250-frame GOP that was 207 ms per frame, against 15.6 ms decoding sequentially. Sources whose codec has no WebCodecs decoder fall back to the previous <video> path automatically.

Custom layer types

The page runs in a separate Chromium realm, so a runtime layer class cannot be handed to it. Register an absolute module path instead and the module is bundled into the renderer page:

import { ServerRenderer } from '@videoflow/renderer-server';

const renderer = new ServerRenderer(json);
renderer.registerLayerType('custom', {
  modulePath: '/absolute/path/to/custom-layer-type.js',
  exportName: 'default',        // optional
});
await renderer.renderVideo({ outputType: 'file', output: './out.mp4' });
await renderer.cleanup();

The module must be browser-compatible and export { runtime, propertiesDefinition }. Register after construction and before the first render — that is when the headless page and its bundle are built. See the browser renderer guide for how to write the runtime class.

Pipelines

Browser export (default)ffmpeg pipeline
EncoderWebCodecs (H.264) + MediaBunny muxJPEG screenshots → libx264 via ffmpeg
AudioEncoded inside the browser, muxed with videoRendered to WAV, muxed by ffmpeg
System depsPlaywright Chromium onlyPlaywright + ffmpeg 4.4+
SpeedSeveral × faster — no per-frame round-tripSlower; useful when you need ffmpeg flags
OutputMP4 (H.264 + AAC)MP4 (configurable via ffmpeg)

Single frame and audio

Need a thumbnail or a music-only export? The renderer exposes per-frame and audio-only methods on the instance:

const renderer = new ServerRenderer(json);
const jpeg = await renderer.renderFrame(120);   // Buffer (JPEG)
const wav  = await renderer.renderAudio();      // Buffer (WAV) | null
await renderer.cleanup();

Inside an HTTP handler

// Express / Node / Bun
app.post('/render', async (req, res) => {
  const json = req.body;
  try {
    const buffer = await ServerRenderer.render(json, { outputType: 'buffer' });
    res.setHeader('Content-Type', 'video/mp4');
    res.setHeader('Content-Disposition', 'attachment; filename="video.mp4"');
    res.end(buffer);
  } catch (err) {
    res.status(500).json({ error: String(err) });
  }
});

Scaling. Render jobs are CPU-heavy. In production, push them into a queue (BullMQ, SQS, Temporal) and cap concurrency to your CPU count. Call closeSharedBrowser() on shutdown — the package keeps a single Chromium instance alive across renders.

VideoFlow

Open-source toolkit for composing videos from code.

Product

CoreRenderersReact Video EditorPlaygroundStudio

Learn

DocsAPI referenceExamplesvs. Remotionvs. FFmpeg

Project

GitHubLicenseContactTermsPrivacy

From the blog

All posts →Building a "Canva for Video" with the @videoflow/react-video-editorAutomating Social Media Captions: A Developer's Guide to Frame-Perfect SubtitlesBeyond Node: How to Render Cinematic Videos from Python and GoBuilding a Video Rendering API with Node.js and VideoFlow (No FFmpeg Required)Mastering Programmatic Video Transitions: A Developer's GuideProgrammatic Video Storage: Why Your MP4s Should Live as Version-Controlled JSONThe Video Markdown Pattern: Turning Static Content into Automated MP4sZero-Cost Video Rendering: How to Export MP4s Directly in the Browser
© 2026 VideoFlow. Apache-2.0 core.