All posts

Videos as React components

Designers were hand-building the same motion story every week in After Effects. Remotion turned it into a component that takes props.

Published
  • Remotion
  • React
  • Node.js
Contents

Every week our designers built five or six motion stories in After Effects. Same template each time: pull the assets, align the text, export, and redo it whenever a name or a photo changed. The work wasn’t creative — it was find-and-replace with a render step.

Remotion

Remotion’s pitch is that you code a video the way you code a website. It renders React frame by frame and stitches the frames into a file. Since the stack was already React, a video became a component that takes props.

export function BadgeStory({ name, photo, badge }: Props) {
  const frame = useCurrentFrame()
  const { fps } = useVideoConfig()

  // spring() for the pop, interpolate() for the fade — both driven by
  // the current frame rather than wall-clock time, so a render is
  // deterministic and a dropped frame cannot desync anything.
  const scale = spring({ frame, fps, config: { damping: 12 } })
  const opacity = interpolate(frame, [0, 20], [0, 1], {
    extrapolateRight: 'clamp',
  })

  return (
    <AbsoluteFill style={{ opacity }}>
      <Img src={photo} style={{ transform: `scale(${scale})` }} />
      <h1>{name}</h1>
      <Audio src={badge.sound} />
    </AbsoluteFill>
  )
}

Props in, video out. A new story is a new row of data, not a new afternoon.

The member announcement story went further — cube rotations at 90° between members, animated backgrounds, counters ticking up. All of it just React, driven by the frame number.

Where it broke

In production, rendering is CPU-bound and Node is single-threaded. Frame-by-frame rendering on the main thread meant a render blocked everything else: API responses crawled and requests started timing out. One person generating a video degraded the service for everyone.

Worker threads, not a queue

The obvious answer was a job queue — BullMQ, Redis, workers, a dashboard. We tried worker threads first.

Each render runs in its own thread, so the main process stays free to serve requests. Ten simultaneous renders no longer touched API latency.

A queue would have added Redis, a worker deployment, job lifecycle handling and a new class of failure, and at our volume it would have solved a problem we didn’t have. Queues earn their keep when you need prioritisation, retries across restarts, or more work than one machine can hold. We needed none of that yet — the point was only to get rendering off the request path.

What’s left

Asset weight is the next bottleneck: WebP for images, H.265 for video. And if volume grows to where renders need prioritising, that’s the point a queue starts paying for itself — not before.