CI & batch rendering
Render thousands of personalized variants on a queue of stateless HyperFrames workers. GitHub Actions, GitLab CI, CircleCI, and plain Docker.
The renderer is stateless. Same input, same bytes. That means horizontal scale is trivial: ship the same Docker image to N workers, point them at the composition plus a variants file, collect the MP4s.
What you'll learn
- The shape of a stateless render queue
- Drop-in pipelines for the four CI systems you probably use
- Realistic throughput per worker — and the bottlenecks that limit it
That's a single 4-vCPU worker rendering plain HTML at H.264 CRF 18 — roughly 20 four-second 1080p60 clips per hour. Three.js and Lottie-heavy comps drop to about 600/hr. Static text + CSS comps climb past 2,400/hr.
Pipeline matrix
Drop one of these into your CI provider. All four assume comp.html is at the repo root and write video.mp4 as an artifact.
# .github/workflows/render.yml
name: render
on: [push]
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hyperframes/render-action@v1
with:
composition: comp.html
out: video.mp4
workers: 4
- uses: actions/upload-artifact@v4
with:
name: video
path: video.mp4Template + variant rendering
The most common batch shape: one HTML template with {{placeholders}}, one JSON file with N rows of substitutions, N MP4s out.
hyperframes render template.html \
--variants variants.json \
--out-dir out/ \
--workers 8variants.json:
[
{ "name": "Asha", "color": "#ff3b1f", "out": "asha.mp4" },
{ "name": "Bilal", "color": "#2b66ff", "out": "bilal.mp4" }
]The renderer dedups frames across variants where it can — if only the title text changes between two MP4s, the background frames are encoded once and reused.
Caching
Two cache layers, both worth wiring up:
- Runtime cache — Chromium boot image and runtime bundle. Shared across renders, baked into the Docker layer.
- Frame cache — keyed by composition hash plus frame index. If one bar moved on a chart, only those frames re-render.
Set HF_CACHE_DIR to a persistent path on runners that wipe /tmp between jobs. The cache survives across runs of the same CI provider as long as the path persists.
Throughput tuning
| Knob | Effect |
|---|---|
--workers N | Cap at physical core count. Encoding is the bottleneck, not capture. |
--codec h264 --crf 23 | Faster than CRF 18, still good enough for most web delivery. |
--fps 30 | Half the frames, roughly half the wall time. |
--width 1280 --height 720 | 720p renders at roughly 2.3x the speed of 1080p. |
Use and avoid
Use the official Docker image — it pins Chromium, FFmpeg, and the CLI together. Use a persistent HF_CACHE_DIR. Use --json and check exit codes; do not parse pretty text.
Avoid mounting node_modules into the container — it's already inside. Avoid --workers higher than your physical core count. Avoid running npm install -g hyperframes inside CI when the Docker image is right there.
Next
- AI agents — the upstream loop that feeds the queue
- Personalized videos at scale — the template-plus-variants pattern in depth