All engineering studies
Owned study 01 / Performance engineering

Give every render less work to do.

Dynamic Product Ads / Creator Renderer

Deployed to production · benchmarked in staging
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.

In this study
  • Node.js / TypeScript
  • Puppeteer / Chromium
  • Canvas / MediaRecorder
  • FFmpeg
  • Redis
  • S3 / Kubernetes
57%less end-to-end render time

65 → 28 seconds for the matched six-second, 30 fps staging workload.

112 MBretained growth eliminated / job

The old per-job leak becomes bounded memory through cleanup and guarded browser recycling.

75%lower peak binary buffers

197 MB → ~50 MB with raw-byte ingest replacing base64 delivery.

92%smaller output file

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.

The engineering, step by step

Follow the system.

Explore the constraint, the decision, how it was checked, and the result. The full narrative follows below.

Follow the systemOwned study 01 / Performance engineering
Constraint / 01

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
  1. SVG composition
  2. WebP + WebMWriter
  3. Base64 transfer
Conceptual sequence · based on the project narrative
The constraint ledger65 → 28s matched staging render · 6-second output
Renderer field notes / 01

Change where the work happens.

The old path encodes snapshots serially. The new path prepares frames first and lets the browser encode while recording.

A / Legacy

Encode every frame in JavaScript.

For a six-second, 30 fps video, CCapture processes 180 frames serially on the page’s main thread.

  1. Compose each frame

    Advance Lottie layers, serialize SVG, decode images and draw the master canvas.

  2. Encode a snapshot

    canvas.toDataURL creates a WebP image with synchronous software encoding.

  3. Assemble the WebM

    WebMWriter extracts VP8 keyframes and appends every frame as a keyframe.

  4. 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.

C / Prerender

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.

  1. Prepare media and bitmaps

    Preload assets and composite timeline samples into an ImageBitmap cache before recording starts.

  2. Record the canvas stream

    captureStream(0) emits on demand. MediaRecorder prefers VP8 at a 4 Mbps target.

  3. Replay at the declared cadence

    Draw a cached bitmap, then request frames at the output fps with a catch-up loop for RAF jitter.

  4. 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.

Why the file shrinks

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.

Renderer field notes / 02

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.

Image macro: 65 s → 28 s

Six-second output at 30 fps. Same template, audio and product on old and new builds; staging with real Elasticsearch, Redis and S3.

Video macro: 405 s → 45.4 s

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.

Image macro / measurement scope

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.

Before 65 s
After 28 s
57% less render time · wall-clock comparison
Image macro measurements
MeasureBeforeAfterReading the result
End-to-end render65 s28 sReported wall-clock total
In-page render47 s22 sNative encoding + prerender
S3 upload12 s6 sSmaller output; measured upload time
Audio + mux1.1 s1.1 sStream copy; audio preparation is cached separately
Peak process RSS385 MB~280 MBApproximately 27% lower peak process memory
Peak binary buffers197 MB~50 MBApproximately 75% lower
Retained memory / job+112 MBBoundedLeak eliminated through cleanup and lifecycle guards
DOM objects / render38,353<1,000Approximately 97% fewer objects
Output file25 MB2.1 MBWebM 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.

Renderer field notes / 03

Stop paying the same setup cost.

Audio, templates and media have different reuse boundaries. Each cache removes a specific repeat-job cost.

01 / Two local tiers / per pod

Brand and creator audio

1.5–2.5 s → ~5 ms warm

Download, 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.
02 / Redis + repeat-job context

Templates and brand records

<10 ms template cache hit

Read 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.
03 / Local disk → browser

Static and product media

Fetch before recording

Fetch 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.
04 / Built / reads disabled

Background plates

Excluded from these results

The 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.
Renderer field notes / 04

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.

Conceptual frame inspector / divisor 4

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.

Frame 1 of 180Cached image 1 of 420.00 s

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.

Prerender divisor measurements
DivisorCached fpsSetupHeadless renderVisual assessment
215~23 s~39 sVery good
310~15 s~28 sGood, accepted
47~9 s~24 sAcceptable; current code value
65~7 s~20 sFlipbook; 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.

Renderer field notes / 05

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.

  1. P0

    Feasibility probes

    Confirm native capture and recording in the pinned Chromium build.

  2. P1

    Flags and legacy parity

    Add the opt-in switch and preserve the existing path as a reference.

  3. P2–P4

    Compositor and recording

    Build RAF compositing, integrate MediaRecorder, defer recycling and define typed failures.

  4. P5

    Rollout controls

    Prepare canary, rollback and runbook artifacts.

  5. P7

    Fast-bitmap path

    Reuse product bitmaps with a transform-matrix fallback for difficult layers.

  6. P8

    Prerender cache

    Move composition before recording and tune the sampling divisor.

  7. After P8

    Benchmark-driven fixes

    Fix nested video-frame URLs, short-layer sampling, frame cadence and the quality gate.

  8. Current

    Production deployment

    The optimized renderer is deployed to production. The timings in this study retain their original staging and local benchmark scopes.

Operating boundaries

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.

Broader capacity result / resume

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