8

I wanted a slow, living gradient behind a hero, the kind that feels expensive. My first attempts animated background-position on a huge gradient and it hitched badly on scroll and cooked laptop fans, because it forces repaint every frame.

The fix was to constrain the model to GPU-friendly properties only (transform/opacity), animate a couple of large blurred radial "blobs" on transforms behind a overflow:hidden container, and require it to explain why each animated property avoids layout/paint. Adding a reduced-motion still frame made it shippable.

Gemini did well once the constraints were explicit. Prompt below. Anyone gotten this smooth without the blurred-blob trick?

THE PROMPT
Create a hero section with an animated gradient background as a single HTML file with inline CSS, no JS, no libraries. Mood: {MOOD e.g. calm, dawn}. Accent hues: {HUE_A}, {HUE_B}.

PERFORMANCE CONSTRAINTS (strict):
- Animate ONLY transform and opacity. Do NOT animate background-position, background-size, filter, or box-shadow (they trigger paint/layout).
- Technique: 2-3 large radial-gradient 'blobs' as absolutely-positioned divs inside an overflow:hidden hero; each blob is blurred once (static filter: blur, not animated) and drifts via translate + subtle scale on different, non-synchronized durations (20-40s) so the motion never obviously loops.
- Add will-change: transform on the blobs; keep total animated nodes <= 3.
- The foreground text sits on a container with its own solid/low-opacity backdrop for contrast; verify text contrast >= 4.5:1 against the busiest point of the gradient.
- prefers-reduced-motion: reduce -> freeze blobs to a pleasant static composition (no animation), still on-brand.
Before the code, in 3 bullets explain why each animated property stays on the compositor and off the main thread. Then the code with real hero copy.
profiled a version of this in devtools and it held 60fps on an old thinkpad. transform+opacity really is the whole secret.kernel_kim 2 months ago
add a comment

1 Answer

4

Compositor-only is the right constraint. Two refinements from shipping these: cap the blur radius relative to viewport (a 200px blur on a 4K screen is a real cost even when static), and offset the blob durations to values that aren't simple multiples of each other so the loop never visually resyncs. I use 23s / 31s / 37s. Prime-ish numbers keep it feeling alive.

THE PROMPT
Set blob animation durations to mutually non-divisible values (e.g. 23s, 31s, 37s) so the composite never resynchronizes, and cap blur at min(120px, 12vw).

Your Answer