the effing blog

Effing 0.38.0: cache me if you can

Effing 0.38.0 adds a persistent image cache for per-frame rendering, plus two fixes: user-space SVG gradients and jitter-free slide motion.

Effing 0.38.0 is out. Here’s what’s new and what got fixed.

The image cache

The headline is a new imageCache option on renderReactElement, the @effing/canvas function that turns a JSX tree into pixels.

By default, every call to renderReactElement creates its own internal image cache. That’s fine when you render one image. But an annie often renders the same component once per frame, and if that component has an <img> or a background-image: url(...), each frame re-fetches and re-decodes that source from scratch. You can work around it by fetching the image yourself, generating a data URL, and using that in the JSX, but that’s rather tedious.

Now you can pass in a cache you own, so each source loads once and every frame after the first reuses it:

import { renderReactElement, type ImageCache } from "@effing/canvas";

const imageCache: ImageCache = new Map();
for (let frame = 0; frame < frameCount; frame++) {
  await renderReactElement(ctx, <Frame n={frame} />, { fonts, imageCache });
}

ImageCache is just a Map, exported for convenience. Two things worth knowing:

  • Concurrent renders share in-flight work. Cache entries are load promises, not resolved images, so two renders that ask for the same source at the same time share a single fetch instead of downloading it twice. You can keep one long-lived cache for your whole render pool.
  • Failed loads don’t poison the cache. A transient network error used to get cached as a rejection, so one blip could break that source for the life of the cache. Now failures are evicted and the next frame tries again.

Sometimes the React tree isn’t the right place for an image, like a full-screen photo background under animated text. For those, the manual’s “Creating Annies” section describes the load-once pattern: pre-load with loadImage(), draw it yourself with ctx.drawImage, and keep the per-frame JSX to text and vectors.

Gradients from your design tool

When rendering SVG, @effing/canvas was treating every gradient’s coordinates as objectBoundingBox fractions, the 0-to-1 kind. But Figma and Illustrator export gradients with gradientUnits="userSpaceOnUse", where the coordinates are real positions in the SVG’s own space. Read as fractions, they clamped to the first stop, so the gradient rendered as a single flat color.

0.38.0 honors userSpaceOnUse for both linear and radial gradients, on fills and strokes, with percentages resolved against the viewport per the SVG spec. Exported icons now render their gradients correctly.

No more slide jitter, FFS

In @effing/ffs, the FFmpeg render service, layers animated with slide motion could pick up a ±2px wobble on individual frames.

It came from two things interacting. FFmpeg’s overlay filter snaps x/y down to even values for yuv420p chroma alignment, and the unrounded position expressions carried enough floating-point noise that a value like 719.9999999 and a clean 720 could land on opposite sides of that boundary from one frame to the next. The motion coordinate expressions for slide, bounce, and shake are now wrapped in round(), so positions hit exact integers before the snap.

Upgrading

Bump your @effing/* packages to 0.38.0. It’s a backward-compatible upgrade and the imageCache option is additive. Per-package details are in the canvas changelog and the ffs changelog.