Gradient, Shadow and Blur Repaint Costs

Part of Paint Invalidation & Repaint Boundaries in Performance Budgeting & GPU Architecture.

The problem

A card design has a soft shadow, a subtle gradient surface, a frosted backdrop behind its header and a glow that intensifies on hover. Individually each is a line of CSS. Together, on a grid of twenty cards, hovering one card produces Paint events of several milliseconds, and scrolling the grid on a phone drops frames even though nothing is animating.

Decoration is where most paint time goes, and the costs are not intuitive: a large blur can cost more than everything else on the page combined.

Root cause analysis: what each effect costs per pixel

Blur is the most expensive. A Gaussian-style blur samples many pixels for each output pixel, and the work grows with the blur radius as well as the area. Doubling the radius costs considerably more than doubling the area. filter: blur(), box-shadow with a large blur, text-shadow and drop-shadow() all pay it.

backdrop-filter pays it repeatedly. It must read what is behind the element, blur it, and composite — and it redoes that whenever the backdrop changes, which during scrolling is every frame, as described in promoting fixed and sticky elements.

Shadows cost blur plus area, and spread enlarges both. A shadow’s painted region is the element’s box grown by spread and blur, so a 40px blur with 12px spread on a 300px card paints a region far larger than the card. Inset shadows are clipped to the box but still blurred.

Gradients are cheaper than blur but not free. A linear gradient is a per-pixel computation; conic and radial gradients cost more, and many stops cost more than two. A full-viewport animated gradient is expensive mostly because of its area.

Solid fills are nearly free. A flat colour is a memory fill.

Stacking multiplies. An element with a gradient background, a blurred shadow and a backdrop filter pays all three every time it repaints.

Relative paint cost of decorating a 320 by 200 cardMid-range phone, one repaint of the card.Relative paint cost of decorating a 320 by 200 cardSolid background0.05 msTwo-stop linear gradient0.2 msConic gradient, six stops0.9 msbox-shadow blur 20px1.4 msbox-shadow blur 60px4.8 msbackdrop-filter blur 12px6.1 ms
Mid-range phone, one repaint of the card.

Step-by-step resolution

Making decoration affordableReduce, pre-paint, or shrink the area.Making decoration affordable1Profile a repaint and rank the draw commands by duration.The dominant decoration2Reduce blur radii; try half the value and compare visually.Large saving, small visual change3Move animated shadows and gradients onto pre-painted layers.Composite instead of paint4Limit backdrop-filter to small surfaces that do not scroll.No per-frame re-blur5Avoid stacking three or more effects on animating elements.One cost, not three6Keep decoration static under reduced motion.
Reduce, pre-paint, or shrink the area.

Production code pattern

/* Expensive: three effects on an element that animates on hover. */
.card-old {
  background: linear-gradient(160deg, var(--color-surface), var(--color-surface-2));
  box-shadow: 0 24px 60px -16px rgb(16 12 32 / 0.5);
  transition: box-shadow 200ms ease-out, translate 200ms ease-out;
}
.card-old:hover { box-shadow: 0 32px 80px -16px rgb(16 12 32 / 0.6); translate: 0 -4px; }

/* Affordable: gradient stays static, shadow is pre-painted and cross-faded. */
.card {
  position: relative;
  isolation: isolate;
  background: linear-gradient(160deg, var(--color-surface), var(--color-surface-2));
  box-shadow: 0 10px 24px -12px rgb(16 12 32 / 0.35);     /* smaller radius at rest */
  transition: translate 200ms ease-out;
}
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  box-shadow: 0 24px 56px -16px rgb(16 12 32 / 0.55);     /* painted once */
  opacity: 0;
  transition: opacity 200ms ease-out;
}
.card:hover { translate: 0 -4px; }
.card:hover::after { opacity: 1; }

/* Frosted header: small, non-scrolling surface only. */
.panel__header {
  backdrop-filter: blur(10px);            /* 260 px wide, static: acceptable */
}
.hero__overlay {
  background: rgb(0 0 0 / 0.45);          /* full-width: solid instead of backdrop-filter */
}

@media (prefers-reduced-motion: reduce) {
  .card, .card::after { transition: none; }
  .card:hover { translate: none; }
}

Rendering Impact: composite for the hover; one paint for the pre-rendered shadow layer. Halving the resting shadow’s blur radius also reduces the cost of every ordinary repaint of the card, including during scrolling.

Reducing blur radius is the least-discussed and most effective change. A 60px blur is rarely twice as beautiful as a 30px blur, and it can cost three times as much. Designers are usually willing to trade a few pixels of softness for smooth scrolling if the comparison is shown side by side.

Decoration, cost driver and cheaper substituteEvery substitute trades some visual fidelity for per-frame cost.Decoration, cost driver and cheaper substituteMain cost driverCheaper substitutebox-shadowBlur radius, then area plus spreadPre-painted layer cross-faded with opacityfilter: blur()Radius and areaPre-blurred image, or smaller radiusbackdrop-filterRadius, area, and backdrop changesTranslucent solid colourLinear gradientAreaKeep static; cross-fade layers to changeConic / radialgradientArea and stop countFewer stops, or a pre-rendered imagetext-shadowRadius and glyph countPre-painted duplicate layer
Every substitute trades some visual fidelity for per-frame cost.

Where the cost is worst

Three situations turn affordable decoration into a problem. Large areas: the same effect on a full-width hero costs an order of magnitude more than on a card. Repeated elements: twenty decorated cards repaint twenty times. And elements that repaint anyway: decoration inside a region that updates frequently — a live list, a sticky header over scrolling content — pays its cost on every one of those repaints, not only during animation.

The remedy for all three is the same as for any paint problem: bound the region with a repaint boundary so the decoration is not redrawn when unrelated content changes, as covered in using contain to scope repaints, and keep the decorated area as small as the design allows.

Verification checklist

Constraints and trade-offs

  • Pre-painted shadow layers need a pseudo-element or wrapper and cost texture memory.
  • Reducing blur changes the design; agree it with designers rather than unilaterally.
  • Translucent solids are flatter than frosted glass.
  • Pre-rendered gradient images do not adapt to theme changes without extra assets.
  • Very small elements can afford effects that would be unaffordable at scale.

Frequently asked questions

Which is more expensive, a large shadow or a large gradient?

The shadow, usually, because blur samples many pixels per output pixel. Gradient cost is closer to proportional to area.

Does reducing blur radius really help?

Substantially. Blur cost grows quickly with radius, so halving the radius often more than halves the cost.

Why is backdrop-filter so much worse than filter?

It has to read and blur the content behind the element, and repeat that whenever the backdrop changes — during scrolling, every frame.

Can I animate a shadow cheaply?

Not directly. Pre-paint the target shadow on a layer and cross-fade it with opacity, which moves the work to the compositor.