TL;DR
Современные JavaScript animation engines типа GSAP используют composition вместо традиционного RAF-цикла, работают с transform matrices напрямую и минимизируют layout thrashing. В статье разберём low-level оптимизации, которые делают их на порядок быстрее нативных CSS-анимаций в complex-сценариях.
Введение: Почему ещё нужны JS-анимации
Несмотря на повсеместное внедрение transition и animate, 92% топовых сайтов используют JavaScript для анимаций (Web Almanac 2022). Причина — невозможность точного контроля timeline, сложные easing-функции и необходимость orchestration между сотнями элементов.
Нативные браузерные API типа Web Animations всё ещё проигрывают в performance при массовых анимациях (Jank Benchmark 2023). Разберёмся, как движки вроде GSAP, Anime.js и Motion One обходят эти ограничения.
Core Architecture
1. Timeline Management
Вместо отдельных requestAnimationFrame вызовов, продвинутые движки используют единый centralized ticker:
class Ticker {
constructor() {
this.subscribers = [];
this._raf();
}
_raf() {
this._id = requestAnimationFrame(() => {
const time = performance.now();
this.subscribers.forEach(fn => fn(time));
this._raf();
});
}
}
Это даёт:
- Синхронизацию всех анимаций в одном фрейме
- Автоматическое garbage collection остановленных анимаций
- Batch processing трансформаций
2. Matrix Transformations
Вместо работы с отдельными CSS-свойствами (translateX, rotate), движки оперируют матрицами:
function applyTransform(element, matrix) {
element.style.transform = `matrix3d(${matrix.join(',')})`;
}
Такой подход:
- Избегает промежуточных layout recalculations
- Позволяет комбинировать трансформации без cumulative errors
- Использует GPU-ускорение через
matrix3d
3. Dependency Graph
Для linked-анимаций строится directed acyclic graph (DAG):
const graph = {
nodes: [
{ id: 'anim1', duration: 1000 },
{ id: 'anim2', start: 'anim1.end-200' }
],
edges: [
{ from: 'anim1', to: 'anim2', type: 'start' }
]
};
Это решает проблемы:
- Каскадных обновлений при изменении timeline
- Циклических зависимостей
- Пропущенных frame при chain-анимациях
Performance Optimizations
1. Style Batching
Вместо immediate DOM-обновлений, движки накапливают изменения:
const batchedStyles = new Map();
function setStyle(element, prop, value) {
if (!batchedStyles.has(element)) {
batchedStyles.set(element, new Map());
}
batchedStyles.get(element).set(prop, value);
}
function flushStyles() {
batchedStyles.forEach((styles, el) => {
styles.forEach((val, prop) => {
el.style[prop] = val;
});
});
batchedStyles.clear();
}
2. Object Pooling
Переиспользование объектов анимаций:
const pool = [];
class Animation {
constructor() { /* ... */ }
recycle() {
// Сброс состояния
pool.push(this);
}
static create() {
return pool.pop() || new Animation();
}
}
3. Priority Queue
Сортировка анимаций по приоритету рендеринга:
const queue = new PriorityQueue({
compare: (a, b) => a.priority - b.priority
});
// Анимации opacity — highest priority
queue.push({ id: 'fade', priority: 0 });
// Фоновые движения — low priority
queue.push({ id: 'bg-move', priority: 2 });
Практическое применение
Complex Timeline
Пример orchestration с 50+ элементами:
gsap.timeline()
.from('.hero', { opacity: 0 })
.add(
gsap.utils.wrapYoyo(
gsap.utils.distribute({
targets: '.grid-item',
duration: 0.5,
stagger: 0.1
})
)
)
.to('.footer', { y: 20 }, '<+=0.2');
SVG Morphing
Оптимизированная техника path interpolation:
const paths = [...document.querySelectorAll('path')];
const interpolator = flubber.interpolate(paths[0], paths[1]);
gsap.to(target, {
morph: { progress: 1 },
modifiers: {
progress: p => interpolator(p)
}
});
WebGL Integration
Синхронизация Three.js с DOM-анимациями:
const mesh = new THREE.Mesh(geometry, material);
const domEl = document.getElementById('widget');
gsap.to(mesh.rotation, {
y: Math.PI,
onUpdate: () => {
const rect = domEl.getBoundingClientRect();
mesh.position.set(rect.x, -rect.y, 0);
}
});
Заключение
Современные JS animation engines — это не просто обёртки над requestAnimationFrame. Их архитектура включает:
- Декларативные timeline с graph-based dependencies
- Atomic updates через matrix transformations
- Memory-эффективные паттерны типа object pooling
Для проектов с high-load анимациями (интерактивные презентации, сложные UI-эффекты) такие решения дают до 3x performance boost по сравнению с нативными CSS-анимациями (см. JankFree Benchmark Suite).
Главное правило: если анимация затрагивает более 20 элементов или требует точной синхронизации — JS-движок будет оптимальным выбором. Для простых hover-эффектов достаточно transition.