JavaScript Animation Engine: Глубже под капот GSAP и аналогов

#javascript#animation#performance#gsap

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();
    });
  }
}

Это даёт:

2. Matrix Transformations

Вместо работы с отдельными CSS-свойствами (translateX, rotate), движки оперируют матрицами:

function applyTransform(element, matrix) {
  element.style.transform = `matrix3d(${matrix.join(',')})`;
}

Такой подход:

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' }
  ]
};

Это решает проблемы:

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. Их архитектура включает:

  1. Декларативные timeline с graph-based dependencies
  2. Atomic updates через matrix transformations
  3. Memory-эффективные паттерны типа object pooling

Для проектов с high-load анимациями (интерактивные презентации, сложные UI-эффекты) такие решения дают до 3x performance boost по сравнению с нативными CSS-анимациями (см. JankFree Benchmark Suite).

Главное правило: если анимация затрагивает более 20 элементов или требует точной синхронизации — JS-движок будет оптимальным выбором. Для простых hover-эффектов достаточно transition.


Источник: https://github.com/sponsors/juliangarnier