Give every render less work to do.
Dynamic Product Ads / Creator Renderer
- Company
- Tyroo Technologies
- Role
- SDE II
- Employment period
- Feb 2023 — Present
This renderer optimization is deployed to production. I replaced serial JavaScript frame encoding with browser-native recording, moved composition into a prerender cache, and removed repeated asset work. In the matched staging benchmark, render time fell from 65 to 28 seconds, with smaller output and bounded per-job memory. Quality checks and independent rollback switches remain in place.
- Node.js / TypeScript
- Puppeteer / Chromium
- Canvas / MediaRecorder
- FFmpeg
- Redis
- S3 / Kubernetes
65 → 28 seconds for the matched six-second, 30 fps staging workload.
The old per-job leak becomes bounded memory through cleanup and guarded browser recycling.
197 MB → ~50 MB with raw-byte ingest replacing base64 delivery.
25 MB → 2.1 MB of WebM bytes, using browser-native inter-frame encoding.
The optimization is deployed to production. The 65 → 28 second result comes from matched image-macro staging runs, not a current production timing. The separate video-macro comparison uses a historical production run and a local replay.
Follow the system.
Explore the constraint, the decision, how it was checked, and the result. The full narrative follows below.
A six-second video took 65 seconds to render.
The old page serialized, decoded and software-encoded every frame before assembling an all-keyframe WebM. Repeated asset preparation and a 112 MB per-job memory leak added separate latency and capacity costs.
- 180 serial frames for six seconds at 30 fps
- All-keyframe output around 25 MB
- +112 MB retained memory per job
- SVG composition
- WebP + WebMWriter
- Base64 transfer
Change where the work happens.
The old path encodes snapshots serially. The new path prepares frames first and lets the browser encode while recording.
Encode every frame in JavaScript.
For a six-second, 30 fps video, CCapture processes 180 frames serially on the page’s main thread.
Compose each frame
Advance Lottie layers, serialize SVG, decode images and draw the master canvas.
Encode a snapshot
canvas.toDataURL creates a WebP image with synchronous software encoding.
Assemble the WebM
WebMWriter extracts VP8 keyframes and appends every frame as a keyframe.
Transfer as base64
The finished blob crosses page.evaluate with about 1.33× byte expansion and extra copies.
Kept as the rollback path and parity reference.
Compose first. Record a cheap replay.
Heavy work moves before recording. Chromium’s native encoder records a canvas stream while the page replays cached bitmaps.
Prepare media and bitmaps
Preload assets and composite timeline samples into an ImageBitmap cache before recording starts.
Record the canvas stream
captureStream(0) emits on demand. MediaRecorder prefers VP8 at a 4 Mbps target.
Replay at the declared cadence
Draw a cached bitmap, then request frames at the output fps with a catch-up loop for RAF jitter.
Deliver raw bytes
POST the WebM blob to a tokenized local ingest server; check frame requests, copy-mux cached audio and upload to S3.
Path B keeps native recording with per-tick compositing when prerender is disabled or exceeds its bitmap budget.
Native VP8 inter-frame compression replaces the legacy all-keyframe output. In the matched benchmark, the file falls from 25 MB to 2.1 MB; the measured S3 upload falls from 12 to 6 seconds.
A result needs its workload.
The optimized renderer is deployed to production. The timings below come from a matched staging benchmark and a separate historical-production/local replay; each keeps its own environment, units and tuning context.
Six-second output at 30 fps. Same template, audio and product on old and new builds; staging with real Elasticsearch, Redis and S3.
A historical production run from 9 May 2026 compared with a local replay on the new build. Square 1080 × 1080 template, seven-second slide, image and video macros. The replay used divisor 6.
The matched staging comparison.
Six-second output at 30 fps. Same template, audio and product on old and new builds; staging with real Elasticsearch, Redis and S3.
| Measure | Before | After | Reading the result |
|---|---|---|---|
| End-to-end render | 65 s | 28 s | Reported wall-clock total |
| In-page render | 47 s | 22 s | Native encoding + prerender |
| S3 upload | 12 s | 6 s | Smaller output; measured upload time |
| Audio + mux | 1.1 s | 1.1 s | Stream copy; audio preparation is cached separately |
| Peak process RSS | 385 MB | ~280 MB | Approximately 27% lower peak process memory |
| Peak binary buffers | 197 MB | ~50 MB | Approximately 75% lower |
| Retained memory / job | +112 MB | Bounded | Leak eliminated through cleanup and lifecycle guards |
| DOM objects / render | 38,353 | <1,000 | Approximately 97% fewer objects |
| Output file | 25 MB | 2.1 MB | WebM bytes; approximately 92% smaller |
Stage timings and the total are reported observations, rather than an additive time breakdown. The rough throughput estimate is ~55 → ~128 videos per worker-hour (2.3×), based on the image-macro render times.
Stop paying the same setup cost.
Audio, templates and media have different reuse boundaries. Each cache removes a specific repeat-job cost.
Brand and creator audio
1.5–2.5 s → ~5 ms warmDownload, trim and transcode to Opus once, then reuse it. The final mux copies both video and audio streams instead of re-encoding.
- Tier 1: URL-hash key; the trimmed file lives with the pod.
- Tier 2: config + creator audio ID, or a config-specific brand-audio key; a three-hour TTL with lazy expiry and a 30-minute sweeper. Copies remain independent of Tier 1.
- A changed audio ID creates a new key. A missing creator audio asset falls back to brand audio.
- Tier 2 is DPA-only. The first render for a config on a new pod pays the cold preparation cost.
Templates and brand records
<10 ms template cache hitRead templates from Redis before calling the creator API. Cache misses fetch and populate the cache; repeat jobs avoid the concurrent API calls that previously timed out after 60+ seconds.
- Template cache access is a separate measurement from end-to-end rendering.
- Brand records use a one-hour cache, with an in-process colors hint to avoid repeated brand lookups.
Static and product media
Fetch before recordingFetch S3 media through the pod’s IAM role and serve it to Chrome locally. A CDN stall during recording can starve the recorder of frames.
- Static template assets are grouped by config and expire after inactivity.
- Dynamic product media uses URL-hash keys and a four-hour TTL.
- Unlocalized assets are reported as CDN fallback. Strict mode turns fallback into a retryable failure.
- Audio and fonts also use the S3 fast path.
Background plates
Excluded from these resultsThe plate generator and cache can prepare a section without the product layer. Reads are disabled and the product-overlay orchestration is unwired in the report.
- The measured improvements come from the active render-path and caching changes above.
Faster still has to look right.
Sampling fewer images saves setup time and memory. Even frame cadence, layer coverage and failure controls protect the output.
180 frame requests.
42 cached images.
A six-second example at 30 output fps samples the timeline at floor(30 ÷ 4) = 7 images per second. Recording reuses those bitmaps. Scrub the expected output frames to see which cached image is selected.
This explains the sampling rule; it is not a video playback or a measured render.
The speed / motion tradeoff.
Report observations on a 1080 × 1080 template. The source uses a hard-coded divisor of 4; changing it requires a rebuild. Declared output stays at 30 fps, with 7 unique cached images per second at that setting.
| Divisor | Cached fps | Setup | Headless render | Visual assessment |
|---|---|---|---|---|
| 2 | 15 | ~23 s | ~39 s | Very good |
| 3 | 10 | ~15 s | ~28 s | Good, accepted |
| 4 | 7 | ~9 s | ~24 s | Acceptable; current code value |
| 6 | 5 | ~7 s | ~20 s | Flipbook; exposed missing short layers |
Count requested frames before delivery.
The recording loop caps requestFrame calls to the declared fps, catches up after RAF jitter and flushes to round(duration × fps). A warm-up paint at time zero starts the video with the animation’s first frame.
The Node-side gate compares requested with expected frames. Below the default 95% ratio, it rejects the render with retry enabled. Compose or decode failures above zero generate warnings. This gate measures frame requests, rather than independently decoding the finished file.
Give every short-lived layer a sample.
High divisors leave larger intervals between cached samples. Short layers, especially after product-specific timeline shifts, could disappear when no sample instant landed inside their visible window.
The interval-overlap gate composites a layer when its visible window overlaps the sample interval and clamps the sampled time into that window. Each visible layer can then land in a cached frame.
Keep video frames loadable inside SVG.
Extracted video frames originally used blob URLs inside SVG images. In the headless page’s null origin, the nested blob URLs had separate opaque origins and the SVG could not load them.
Inline JPEG data URIs at quality 0.85 remove that nested fetch and origin dependency. The local video-macro benchmark revealed this broken-image failure.
Bound memory before allocating the cache.
Projected bitmap memory is frame count × width × height × four bytes. The source sets a 1.5 GiB budget; an oversized prerender falls back to native recording with per-tick compositing (Path B).
Path B’s fast-bitmap compositor reuses product ImageBitmaps and a small SVG. Null or degenerate transform matrices fall back to the heavier compositor. Path C prerender uses that heavier path for correctness.
Transfer bytes without duplicating them.
A local Node ingest server accepts the raw video blob with a per-render token and a 64 MB cap. It replaces the base64 return through page.evaluate, reducing expansion and large in-memory copies.
The matched benchmark reports peak binary-buffer memory falling from 197 MB to roughly 50 MB. A separate switch retains the base64 delivery fallback.
Recycle a browser only when the job is safe.
The shared browser is bounded by a default five-job recycle policy. An active-jobs counter prevents recycling during a job; the recording-active bridge delays a pending recycle until MediaRecorder stops.
Per-job cleanup closes pages, removes temporary directories and terminates FFmpeg child processes, escalating from SIGTERM to SIGKILL on abort. Together with fewer per-frame DOM objects and binary copies, this removed retained growth of 112 MB per job.
Handle recorder failures explicitly.
MIME negotiation prefers VP8 WebM, then VP9, then generic WebM. Unsupported MIME, startup errors, recorder errors and stalls have typed failure kinds.
The stall timeout is the video duration plus five seconds and is cancelled by the first data chunk. Audio is added later on the Node side. Independent switches allow native capture, prerender, fast-bitmap compositing, config-audio caching and binary delivery to be rolled back separately.
Ship the change with a way back.
The optimized renderer is now deployed to production. The work progressed through feasibility, parity checks, recording, canary controls and benchmark-driven fixes. Legacy rendering remains a reference and fallback.
- P0
Feasibility probes
Confirm native capture and recording in the pinned Chromium build.
- P1
Flags and legacy parity
Add the opt-in switch and preserve the existing path as a reference.
- P2–P4
Compositor and recording
Build RAF compositing, integrate MediaRecorder, defer recycling and define typed failures.
- P5
Rollout controls
Prepare canary, rollback and runbook artifacts.
- P7
Fast-bitmap path
Reuse product bitmaps with a transform-matrix fallback for difficult layers.
- P8
Prerender cache
Move composition before recording and tune the sampling divisor.
- After P8
Benchmark-driven fixes
Fix nested video-frame URLs, short-layer sampling, frame cadence and the quality gate.
- Current
Production deployment
The optimized renderer is deployed to production. The timings in this study retain their original staging and local benchmark scopes.
The extension is not the container.
DPA objects use a .mp4 key and video/mp4 metadata, while the output bytes are VP8/Opus WebM. There is no H.264 transcode in the documented path. A consumer requiring a true MP4 container needs a compatible output path.
Enablement is environment-specific.
The optimized renderer is deployed to production. Native capture and prerender remain feature-gated; the report does not establish which flags are active for every job or environment. Legacy CCapture remains available for rollback, and the audio caches warm independently on each pod.
Coverage includes a known gap.
Contract tests cover flags, prerender, fast-bitmap compositing and media preload. The report records missing unit coverage for real video-frame extraction; the nested-URL bug surfaced during benchmarking.
My updated resume also reports that renderer infrastructure was halved at constant volume. That describes the broader capacity outcome; the workload-specific timings above are the optimization report’s comparisons.
Have a similar systems challenge?
Let’s talk