List Animation Performance
Use will-change CSS property to optimize GPU-accelerated animations in dynamic lists.
List Animation Performance
Problem
Animating list items (fade in, slide, expand) can cause jank and dropped frames because the browser must recalculate layouts and compositing on every frame.
Solution
Use the CSS property will-change to hint the browser about upcoming animations. This promotes the element to its own GPU layer, enabling hardware-accelerated rendering.
type AnimationStyle = 'fade' | 'slide' | 'scale' | 'none';
interface ListItemAnimation {
element: HTMLElement;
style: AnimationStyle;
duration: number;
}
class ListAnimationManager {
private animatingElements = new Set<HTMLElement>();
/**
* Prepare an element for animation by adding will-change
* This should be called BEFORE the animation starts
*/
private prepareAnimation(element: HTMLElement, properties: string[]): void {
element.style.willChange = properties.join(', ');
this.animatingElements.add(element);
}
/**
* Clean up will-change after animation completes
* Keeping will-change active consumes memory
*/
private cleanupAnimation(element: HTMLElement): void {
element.style.willChange = 'auto';
this.animatingElements.delete(element);
}
/**
* Animate a single list item
*/
public animateItem(config: ListItemAnimation): Promise<void> {
return new Promise((resolve) => {
const { element, style, duration } = config;
// Set up animation properties based on style
let properties: string[] = [];
let animationClass = '';
switch (style) {
case 'fade':
properties = ['opacity'];
animationClass = 'fade-in';
break;
case 'slide':
properties = ['transform', 'opacity'];
animationClass = 'slide-in';
break;
case 'scale':
properties = ['transform', 'opacity'];
animationClass = 'scale-in';
break;
default:
resolve();
return;
}
// Prepare element for animation
this.prepareAnimation(element, properties);
// Small delay to ensure will-change is applied
requestAnimationFrame(() => {
element.style.animationDuration = `${duration}ms`;
element.classList.add(animationClass);
// Clean up after animation
const cleanup = (): void => {
this.cleanupAnimation(element);
element.removeEventListener('animationend', cleanup);
resolve();
};
element.addEventListener('animationend', cleanup, { once: true });
});
});
}
/**
* Animate multiple items with stagger effect
*/
public async animateList(
items: HTMLElement[],
style: AnimationStyle,
duration: number,
staggerDelay: number
): Promise<void> {
const animations = items.map((element, index) => {
return new Promise<void>((resolve) => {
setTimeout(() => {
this.animateItem({ element, style, duration }).then(resolve);
}, index * staggerDelay);
});
});
await Promise.all(animations);
}
/**
* Force cleanup all animations (useful on unmount)
*/
public cleanup(): void {
this.animatingElements.forEach((element) => {
this.cleanupAnimation(element);
});
}
}
// CSS to be included in your stylesheet
const animationStyles = `
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-in {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes scale-in {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.fade-in {
animation-name: fade-in;
animation-timing-function: ease-out;
animation-fill-mode: both;
}
.slide-in {
animation-name: slide-in;
animation-timing-function: ease-out;
animation-fill-mode: both;
}
.scale-in {
animation-name: scale-in;
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
animation-fill-mode: both;
}
`;
// Practical usage
function renderListWithAnimation(
container: HTMLElement,
items: string[]
): ListAnimationManager {
const manager = new ListAnimationManager();
// Create list items
const elements = items.map((text) => {
const li = document.createElement('li');
li.textContent = text;
li.style.opacity = '0'; // Start invisible
container.appendChild(li);
return li;
});
// Animate with stagger
manager.animateList(elements, 'slide', 400, 50);
return manager;
}Performance Note
The will-change property tells the browser to prepare for changes, enabling GPU acceleration. This results in:
- Smooth 60fps animations: offloading work to the GPU
- No layout thrashing: elements on separate layers don't trigger reflows
- Reduced paint time: compositing happens on the GPU
Important: Only use will-change during animations. Keeping it active consumes memory because the browser maintains separate GPU layers. Always clean up with will-change: auto after the animation completes.
For staggered list animations (items appearing one after another), use small delays (50-100ms) between items. This creates a polished, professional feel without making users wait.