Animation Performance Checker
Lighthouse says “avoid non-composited animations” but never tells you which ones or how to fix them. This tool parses every @keyframes on your page, flags the CPU-heavy ones and gives an exact transform/opacity conversion for each.
How does the Animation Performance Checker work?
It fetches your page’s HTML and CSS in your browser — inline styles plus up to 6 external stylesheets, including cross-origin CDNs — parses every @keyframes block, and classifies each animated property: transform and opacity are GPU-composited (cheap), paint properties like color and filter repaint every frame (moderate), and layout properties like left, width or background-position reflow the page every frame (expensive). It also catches duplicate @keyframes names — a sneaky bug where the last definition silently wins. It reads prefixed @-webkit-keyframes and first-level @import files too, and if a stylesheet resists fetching you can paste the .css file URL directly as the input.
- Enter a URL — ideally the page Lighthouse flagged.
- Check the GPU-friendly score — the share of animations that are composited.
- Open each flagged keyframe — it lists the offending properties and which rules use it.
- Apply the suggested conversion — same visual effect, transform/opacity only.
- Gate ambient loops on mobile and add a prefers-reduced-motion block.
Which CSS properties are safe to animate?
Only transform and opacity are fully GPU-composited — they skip layout and paint entirely and stay smooth at 60fps even on cheap phones. Everything else costs main-thread time every single frame: paint properties repaint the element, and layout properties reflow the whole page. The table below shows the standard conversions.
| Instead of animating… | Use this composited equivalent |
|---|---|
| left / top / right / bottom | transform: translateX() / translateY() |
| width / height | transform: scaleX() / scaleY() |
| background-position (gradient flow) | Make the element ~300% wide with a seamless gradient, animate translateX(-66.667%) |
| box-shadow pulse | transform: scale() pulse, or shadow on a ::after with opacity |
| border-color / SVG fill | Static colour on the class + opacity in the keyframes |