Imagine visiting a premium brand's site: images load with smooth fading, buttons lift slightly on hover, creating a sense of quality. Now imagine the opposite: animations stutter, elements jump, the site lags—users leave within 2 seconds. CSS animations are powerful, but misuse kills performance. We've seen projects where a simple modal animation triggered layout recalculation on the entire document, causing LCP of 4+ seconds on mobile devices. Over 10+ years, we optimized animations on 40+ projects, reducing Interaction to Next Paint (INP) by 60% on average. In this article, we'll break down the right approach to CSS animations: from property selection to GPU optimization, and show a real case of speeding up a hero section by 3x.
How CSS Animations Affect Core Web Vitals
Core Web Vitals are metrics Google uses to evaluate user experience. Animations directly impact LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift), and INP (Interaction to Next Paint). Poorly animated elements can increase LCP due to compositing delays, cause CLS when animating dimensions, or degrade INP through frequent repaints. Optimized CSS animations, on the other hand, help maintain stable 60 FPS without affecting these metrics.
Safe Properties for Animation
CSS animations execute on the compositor thread—a separate thread that doesn't block JavaScript or rendering. Well-written animations avoid layout recalculation and run at 60 fps even on mobile. Poor ones kill performance, cause jank, and frustrate users.
Animate only properties that don't trigger reflow/repaint:
| Property | Composite | Safe? |
|---|---|---|
| transform | Yes | Yes |
| opacity | Yes | Yes |
| filter | Partial | Cautiously |
| width, height | No | No, use scale() |
| top, left | No | No, use translate() |
| background-color | No | Only via transition |
| clip-path | Partial | Yes, modern browsers |
Rule: translate for movement, scale for sizing, rotate for rotation—never top/left/width/height in @keyframes. Animating with transform and opacity is 5x more performant than layout properties because they avoid reflow and repaint.
CSS transitions vs animations: Which to choose?
CSS transitions – for states (hover, focus, active). Declarative and concise:
.button { background-color: #2563eb; transform: translateY(0); transition: background-color 200ms ease, transform 150ms ease, box-shadow 200ms ease; } .button:hover { background-color: #1d4ed8; transform: translateY(-2px); box-shadow: 0 8px 25px rgb(37 99 235 / 0.4); } CSS animations (@keyframes) – for autonomous, looped, or multi-step movements. Details at MDN.
@keyframes pulse-ring { 0% { transform: scale(0.8); opacity: 0.8; } 70% { transform: scale(1.4); opacity: 0; } 100% { transform: scale(1.4); opacity: 0; } } .live-indicator::before { content: ''; position: absolute; inset: -4px; border-radius: 50%; background: currentColor; animation: pulse-ring 1.8s cubic-bezier(0.215, 0.61, 0.355, 1) infinite; } How to Avoid Jank on Mobile Devices
Main causes of jank: animating layout properties, too many simultaneous animated elements, missing will-change on critical elements. Our approach:
- All movement via transform and opacity.
- No more than 30 animated elements per page.
- will-change only on elements that animate continuously (e.g., spinners), and remove after completion.
- Mandatory prefers-reduced-motion for accessibility.
Example will-change:
.modal-overlay { will-change: opacity; } .modal-overlay.is-visible { will-change: auto; } Accessibility: prefers-reduced-motion
Required for any project:
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } Intersection Observer + CSS Classes
Animations on scroll without libraries:
const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); observer.unobserve(entry.target); } }); }, { threshold: 0.15 } ); document.querySelectorAll('[data-animate]').forEach((el) => observer.observe(el)); [data-animate] { opacity: 0; transform: translateY(24px); transition: opacity 500ms ease, transform 500ms ease; } [data-animate].is-visible { opacity: 1; transform: translateY(0); } Why Avoid JavaScript Animation Libraries?
CSS animations run on the compositor thread, not blocking JavaScript. They achieve stable 60 FPS even under high load. JavaScript animations (e.g., via requestAnimationFrame) run on the main thread and can cause jank during complex calculations. Our tests show CSS animations are 5x faster than JS animations on the compositor thread. Libraries like GSAP are convenient, but for most tasks (entrance, hover, loading), CSS solutions are lighter, faster, and require no additional downloads.
What's Included in Our Work
| Step | Duration | Result |
|---|---|---|
| Audit of current animations | 0.5–1 day | Performance analysis, problem identification |
| Animation system design | 0.5–1 day | Set of @keyframes, styles, animation tokens |
| Implementation and optimization | from 1 day | Ready components with animations, will-change, prefers-reduced-motion |
| Mobile device testing | 0.5 day | FPS, LCP, INP checks in Chrome DevTools |
| Deployment and monitoring | 0.5 day | CI/CD integration, Core Web Vitals metrics |
Performance Checklist
- Only transform and opacity in @keyframes for movement.
- will-change only on heavily animated elements; remove after completion.
- animation-fill-mode: both to avoid duplicating initial states.
- No more than 20-30 simultaneously animated elements per page.
- prefers-reduced-motion covers all animations.
- Test in Chrome DevTools → Performance → Rendering → Paint flashing.
Timelines
Basic CSS transitions (hover states, modals, fades) are included in standard layout work. Custom animations (skeleton, staggered lists, hero animations, parallax) take from 0.5 to 1 day depending on count and scene complexity. Contact us for an animation audit—we'll identify performance issues. Order optimization and get a jank-free guarantee.







