@remotion/browser-bundlerv4.0.527
Compile a virtual Remotion project in the browser and use a registered composition in your own UI, such as a <Player>.
Draft API: This API is not yet stable. We are in the experimental phase of this package and reserve to change it at any time.
The initial implementation has only been tested in Chrome.
The main entry point compiles source snapshots, manages its worker and WebAssembly compiler, applies source edits incrementally, and resolves npm imports over HTTP. The separate @remotion/browser-bundler/runtime entry point executes a bundle and resolves a registered composition.
Neither entry point provides an editor or playback UI.
Installation
- Remotion CLI
- npm
- bun
- pnpm
- yarn
npx remotion add @remotion/browser-bundler
This assumes you are currently using v4.0.539 of Remotion.npm i --save-exact @remotion/[email protected]
Also update
remotion and all `@remotion/*` packages to the same version.Remove all
^ character in front of the version numbers of it as it can lead to a version conflict.This assumes you are currently using v4.0.539 of Remotion.pnpm i @remotion/[email protected]
Also update
remotion and all `@remotion/*` packages to the same version.Remove all
^ character in front of the version numbers of it as it can lead to a version conflict.This assumes you are currently using v4.0.539 of Remotion.bun i @remotion/[email protected]
Also update
remotion and all `@remotion/*` packages to the same version.Remove all
^ character in front of the version numbers of it as it can lead to a version conflict.This assumes you are currently using v4.0.539 of Remotion.yarn --exact add @remotion/[email protected]
Also update
remotion and all `@remotion/*` packages to the same version.Remove all
^ character in front of the version numbers of it as it can lead to a version conflict.For the example below, also install @remotion/player. Keep all Remotion packages on the same version, and use the same React and React DOM versions in the host app and virtual project.
Browser setup
Serve your app in a secure context, such as HTTPS or localhost, with these response headers:
HTTP response headersCross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
window.crossOriginIsolated must be true. The browser must support module workers, SharedArrayBuffer, and WebAssembly with shared memory. Cross-origin scripts, media, and other resources must satisfy the page's cross-origin policy.
The compiler downloads a sizable WebAssembly binary and can use significant memory. The first compilation is slower than subsequent edits. Use onProgress to display download progress, and reuse a bundler instance while editing.
Host bundler support
The package ships a prebuilt compiler worker together with its WebAssembly binary and supporting worker asset. Your app's bundler must handle new Worker(new URL(..., import.meta.url)) and emit the assets referenced with new URL(..., import.meta.url). Serve those emitted assets alongside your app.
Alternatively, use workerUrl to host the packaged worker and its adjacent assets at a custom same-origin location. Preserve the packaged filenames and relative paths. This overrides the main worker URL, not the requirement to deploy its supporting assets. See serving the compiler as static files for a setup that does not depend on the host bundler.
Other bundlers and deployment configurations are not guaranteed to work without additional setup. Call these APIs on the client, not during server rendering.
Next.js
For TypeScript projects, use "moduleResolution": "bundler" in tsconfig.json so imports from @remotion/browser-bundler/runtime resolve through the package's exports.
Add the cross-origin isolation headers to the routes that use the compiler, including the compiler assets. This example uses /browser-bundler as the page route; replace it with your route.
next.config.mjs/** @type {import('next').NextConfig} */ const nextConfig = { async headers() { return ['/browser-bundler', '/_next/:path*', '/compiler/:path*'].map( (source) => ({ source, headers: [ {key: 'Cross-Origin-Opener-Policy', value: 'same-origin'}, {key: 'Cross-Origin-Embedder-Policy', value: 'require-corp'}, ], }), ); }, }; export default nextConfig;
If other pages are served without these headers, use full-page navigation when entering or leaving this route. Next.js client-side navigation does not change the current document's cross-origin isolation. A normal <a> link performs a full-page navigation.
Serving the compiler as static filesv4.0.529
The recommended setup works with Turbopack and webpack, in the App Router and the Pages Router: copy the compiler assets into public/ and point workerUrl at them. The worker resolves its WebAssembly binary and supporting worker relative to its own URL, so the Next.js bundler does not process them.
scripts/copy-compiler.mjsimport {copyFile, mkdir} from 'node:fs/promises'; import {createRequire} from 'node:module'; import path from 'node:path'; const require = createRequire(import.meta.url); const dist = path.join( path.dirname(require.resolve('@remotion/browser-bundler/package.json')), 'dist', ); const target = path.join(process.cwd(), 'public', 'compiler'); await mkdir(target, {recursive: true}); for (const asset of [ 'browser-bundler-worker.js', 'rspack.wasm32-wasi.wasm', 'wasi-worker-browser.mjs', ]) { await copyFile(path.join(dist, asset), path.join(target, asset)); }
Run the script before next dev and next build, and ignore public/compiler in version control. Then create the bundler with the served worker:
compiler.tsimport {createBrowserBundler } from '@remotion/browser-bundler'; constbundler =createBrowserBundler ({workerUrl : newURL ('/compiler/browser-bundler-worker.js',window .location .origin ), });
Without workerUrl, the package falls back to new Worker(new URL('./browser-bundler-worker.js', import.meta.url)), which makes the Next.js bundler process the worker and its assets. In the App Router, the supporting worker is then transpiled and fails to start with Cannot use 'import.meta' outside a module, and Turbopack warns about a module format mismatch.
Bundling the compiler with webpack
In the Pages Router with webpack, the compiler assets can be bundled by Next.js instead. Use webpack for development and production builds, not Turbopack:
package.json (scripts){ "scripts": { "dev": "next dev --webpack", "build": "next build --webpack" } }
Merge these rules into your configuration:
next.config.mjs/** @type {import('next').NextConfig} */ const nextConfig = { webpack(config) { config.module.rules.push( {test: /browser-bundler-worker\.js$/, parser: {url: true}}, {test: /\.wasm$/, type: 'asset/resource'}, {test: /wasi-worker-browser\.mjs$/, type: 'asset/resource'}, ); return config; }, }; export default nextConfig;
Next.js normally makes asset URLs relative. The supporting worker starts from a blob: URL, so its importScripts() call needs an absolute script URL. The scoped parser: {url: true} rule preserves that behavior for the packaged compiler worker without changing URL handling throughout your app.
This configuration does not work in the App Router, where the supporting worker is passed through the React Refresh loader before the asset/resource rule applies. Use the static files setup above instead.
Trusted code only
loadBrowserBundle() evaluates JavaScript using Function. The bundle and its dependencies run with your host page's privileges, including access to its DOM, storage, and network. This is not a sandbox; do not use it to execute untrusted source.
Your Content Security Policy must permit JavaScript code evaluation ('unsafe-eval'), workers, and WebAssembly compilation, as well as the asset and dependency requests. Allowing only 'wasm-unsafe-eval' is insufficient for the runtime's JavaScript evaluation.
Minimal example
This is a normal Remotion project: its entry point calls registerRoot(), and its root registers a <Composition>. The host selects the hardcoded HelloWorld ID after compiling.
BrowserBundlerPreview.tsx'use client'; import {createBrowserBundler , typeBrowserBundler , typeVirtualProject , } from '@remotion/browser-bundler'; import {getBrowserComposition ,loadBrowserBundle , typeBrowserComposition , } from '@remotion/browser-bundler/runtime'; import {Player } from '@remotion/player'; import {useEffect ,useState } from 'react'; constproject :VirtualProject = {entryPoint : 'src/index.ts',files : { 'src/index.ts': ` import {registerRoot} from 'remotion'; import {Root} from './Root'; registerRoot(Root); `, 'src/Root.tsx': ` import {AbsoluteFill, Composition, useCurrentFrame} from 'remotion'; const Video = ({message}: {message: string}) => { const frame = useCurrentFrame(); return ( <AbsoluteFill style={{ backgroundColor: 'white', color: 'black', justifyContent: 'center', alignItems: 'center', fontSize: 80, opacity: Math.min(frame / 30, 1), }}> {message} </AbsoluteFill> ); }; export const Root = () => ( <Composition id="HelloWorld" component={Video} durationInFrames={90} fps={30} width={1280} height={720} defaultProps={{message: 'Hello world'}} /> ); `, }, }; export constBrowserBundlerPreview = () => { const [composition ,setComposition ] =useState <BrowserComposition | null>(null); const [error ,setError ] =useState <string | null>(null);useEffect (() => { constcontroller = newAbortController (); letbundler :BrowserBundler | null = null; constcompile = async () => { try {bundler =createBrowserBundler (); constbundle = awaitbundler .bundle ({project }); if (controller .signal .aborted ) { return; } constroot =loadBrowserBundle ({bundle }); constresolved = awaitgetBrowserComposition ({root ,compositionId : 'HelloWorld',inputProps : {message : 'Hello from the browser!'},signal :controller .signal , }); if (!controller .signal .aborted ) {setComposition (resolved ); } } catch (err ) { if (!controller .signal .aborted ) {setError (err instanceofError ?err .message :String (err )); } } finally {bundler ?.dispose (); } }; voidcompile (); return () => {controller .abort ();bundler ?.dispose (); }; }, []); if (error ) { return <pre >{error }</pre >; } if (!composition ) { return <p >Compiling...</p >; } return ( <Player component ={composition .component }inputProps ={composition .props }durationInFrames ={composition .durationInFrames }fps ={composition .fps }compositionWidth ={composition .width }compositionHeight ={composition .height }controls style ={{width : '100%'}} /> ); };
Pass the returned component to <Player>, not the registered root or <Composition> itself. Pass props and the resolved dimensions, frame rate, and duration alongside it. getBrowserComposition() merges default and input props and runs calculateMetadata() when provided.
This example compiles once and disposes its worker. For editing, keep the bundler alive and submit a new complete snapshot when the source changes. Keep the last successful preview visible while compiling.
Scope and limitations
Source snapshots
A VirtualProject contains project-relative source file paths, such as src/index.ts and src/Root.tsx, and their text. You own editing, history, saving, and restoring snapshots. The package does not provide filesystem persistence or an installation UI for npm packages.
Dependencies
Browser-compatible npm imports resolve through esm.sh. Default dependency versions come from the versions used to build the package. Use dependencyVersions to pin additional dependencies.
React, React DOM, and Remotion imports are shared with the host app when executing the bundle; they are not isolated copies. An override cannot make a different React or Remotion version run alongside the host version.
Public assets
The virtual project has no public/ asset mapping or public-file persistence. staticFile() does not map a virtual source file to a served asset. Use hosted asset URLs that satisfy the page's cross-origin policy.
Updating the preview
Compiling does not automatically replace an existing Player component. You own recompilation triggers, loading and resolving the new bundle, handling stale results and errors, and updating playback.
For state-preserving edits, enable enableFastRefresh and apply bundles with createBrowserBundleRuntime(). Use an isolated preview with development React and createBrowserCompositionObserver() to keep the composition registration tree mounted alongside the Player. This uses Rspack hot updates and React Fast Refresh rather than evaluating and remounting a fresh Player on each edit.
Coalesce pending source edits before compilation, but apply every successfully compiled Fast Refresh bundle in order. Discarding compiled updates breaks the hot-update chain.
This package does not export a video file. See client-side rendering for video export APIs and their separate requirements.