Worklet animacji Houdiniego

Zwiększanie wydajności animacji w aplikacji internetowej

TL;DR: moduł pracy animacji umożliwia tworzenie animacji imperatywnej, które działają z rodzinną częstotliwością odświeżania urządzenia, co zapewnia dodatkową płynność™, czyni animacje bardziej odpornymi na problemy z głównym wątkiem i umożliwia ich łączenie z przewijaniem zamiast czasu. Worklet animacji jest dostępny w Chrome Canary (za flagą „Eksperymentalne funkcje platformy internetowej”), a w przyszłości planujemy testowanie origin w Chrome 71. Możesz zacząć z niego korzystać jako z funkcji ulepszania stopniowego już dziś.

Inny interfejs Animation API?

Nie, to rozszerzenie tego, co już mamy, i to nie bez powodu. Zacznijmy od początku. Jeśli chcesz animować dowolny element DOM w internecie, masz 2 i 1/2 opcji: przejścia CSS do prostych przejść A–B, animacja CSS do potencjalnie cyklicznych, bardziej złożonych animacji opartych na czasie oraz interfejs API Web Animations (WAAPI) do dowolnie złożonych animacji. Macierz obsługi WAAPI wygląda dość źle, ale rośnie. Do tego czasu możesz użyć polyfilla.

Wszystkie te metody mają to wspólne, że są bezosobowe i zorientowane na czas. Jednak niektóre efekty, które próbują uzyskać deweloperzy, nie są ani zależne od czasu, ani stanowe. Na przykład osławiony scroller z efektem paralaksy działa, jak sama nazwa wskazuje, na przewijanie. Wdrażanie wydajnego scrollera paralaksy w internecie jest obecnie zaskakująco trudne.

A co z państwem? Pomyśl na przykład o pasku adresu Chrome na Androidzie. Jeśli przewiniesz w dół, zniknie z pola widzenia. Gdy przewiniesz w górę, wróci ono na swoje miejsce, nawet jeśli jesteś w połowie strony. Animacja zależy nie tylko od pozycji przewijania, ale też od poprzedniego kierunku przewijania. Jest to stanowa.

Innym problemem jest stylizacja suwaków. Nie można ich stylizować – a przynajmniej nie na tyle, na ile byśmy chcieli. Co zrobić, jeśli chcę mieć kota Nyan jako suwaka? Niezależnie od wybranej metody tworzenie niestandardowego paska przewijania nie jest ani wydajne, ani łatwe.

Chodzi o to, że wszystkie te kwestie są trudne do wdrożenia w sposób efektywny. Większość z nich korzysta z zdarzeń lub funkcji requestAnimationFrame, które mogą zapewnić płynność obrazu na poziomie 60 FPS, nawet jeśli Twój ekran obsługuje 90 FPS, 120 FPS lub wyższą wartość, i wykorzystywać ułamek cennego budżetu wątku głównego.

Worklet animacji rozszerza możliwości pakietu animacji w internecie, aby ułatwić tworzenie tego typu efektów. Zanim przejdziemy do omawiania animacji, sprawdźmy, czy znasz podstawy.

Wprowadzenie do animacji i osi czasu

WAAPI i Animation Worklet korzystają z osi czasu, aby umożliwić Ci sterowanie animacjami i efektami w wybrany sposób. W tej sekcji znajdziesz krótkie przypomnienie lub wprowadzenie do osi czasu i ich działania w przypadku animacji.

Każdy dokument ma document.timeline. Wartość ta wynosi 0 w momencie utworzenia dokumentu i jest zliczana w milisekundach od tego momentu. Wszystkie animacje w dokumencie działają w odniesieniu do tej osi czasu.

Aby to zilustrować, przyjrzyjmy się temu fragmentowi kodu WAAPI:

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

Gdy wywołujemy funkcję animation.play(), animacja używa jako czasu rozpoczęcia wartości currentTime z osi czasu. Nasza animacja ma opóźnienie 3000 ms, co oznacza, że animacja rozpocznie się (lub stanie się „aktywna”), gdy linia czasu osiągnie wartość startTime.

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. Chodzi o to, że linia czasu określa, na jakim etapie animacji jesteśmy.

Gdy animacja dotrze do ostatniej klatki kluczowej, przeskoczy do pierwszej klatki kluczowej i rozpocznie kolejną iterację animacji. Ten proces powtarza się łącznie 3 razy od momentu ustawienia iterations: 3. Jeśli chcemy, aby animacja nigdy się nie zatrzymywała, wpisujemy iterations: Number.POSITIVE_INFINITY. Oto wynik wykonania kodu podanego powyżej.

WAAPI jest niesamowicie potężnym interfejsem API, który zawiera wiele innych funkcji, takich jak łagodnienie, przesunięcia początkowe, wagi klatek keyframe i zachowanie wypełniania, które wykraczają poza zakres tego artykułu. Jeśli chcesz dowiedzieć się więcej, przeczytaj ten artykuł na temat animacji CSS na stronie CSS Tricks.

Tworzenie animacji Worklet

Teraz, gdy już znamy podstawy osi czasu, możemy przyjrzeć się elementom animacji i temu, jak można nimi manipulować na osi czasu. Interfejs Animation Worklet API nie tylko opiera się na WAAPI, ale jest też – w rozumieniu rozszerzalnej sieci – prymitywem niższego poziomu, który wyjaśnia, jak działa WAAPI. Pod względem składni są bardzo podobne:

Worklet animacji WAAPI
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

Różnica polega na pierwszym parametrze, który jest nazwą elementu roboczego, który obsługuje tę animację.

Wykrywanie cech

Chrome jest pierwszą przeglądarką, która obsługuje tę funkcję, więc musisz się upewnić, że Twój kod nie oczekuje tylko AnimationWorklet. Dlatego przed załadowaniem workletu należy sprawdzić, czy przeglądarka użytkownika obsługuje AnimationWorklet. Aby to zrobić, wystarczy wykonać tę prostą czynność:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Wczytywanie workleta

Worklety to nowa koncepcja wprowadzona przez zespół zadaniowy Houdini, która ułatwia tworzenie i skalowanie wielu nowych interfejsów API. Szczegóły dotyczące workletów omówimy nieco później, ale na razie możesz je traktować jako tanie i lekkie wątki (jak wątki robocze).

Zanim zadeklarujesz animację, musisz się upewnić, że został załadowany element worklet o nazwie „passthrough”:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

Co się tutaj dzieje? Rejestrujemy klasę jako animatora za pomocą wywołania registerAnimator() AnimationWorklet, nadając jej nazwę „passthrough”. To ta sama nazwa, której użyliśmy w konstruktorze WorkletAnimation(). Po zakończeniu rejestracji obietnica zwrócona przez addModule() zostanie rozwiązana i będziemy mogli zacząć tworzyć animacje za pomocą tego workletu.

Metoda animate() naszego wystąpienia zostanie wywołana w przypadku każdego kadru, który przeglądarka chce renderować, przekazując currentTime osi czasu animacji, a także efekt, który jest obecnie przetwarzany. Mamy tylko jeden efekt, KeyframeEffect, i używamy currentTime do ustawienia localTime efektu, dlatego ten animator nazywa się „passthrough”. Dzięki temu kodowi worklet, WAAPI i AnimationWorklet działają dokładnie tak samo, jak widać to na demo.

Godzina

Parametr currentTime metody animate() to currentTime z czasowej osi, która została przekazana do konstruktora WorkletAnimation(). W poprzednim przykładzie po prostu przekazaliśmy ten czas do efektu. Ponieważ jednak jest to kod JavaScript, możemy zniekształcać czas 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Bierzemy Math.sin()currentTime i przypisujemy tę wartość do przedziału [0; 2000], czyli zakresu czasu, w którym zdefiniowany jest efekt. Teraz animacja wygląda zupełnie inaczej, mimo że nie zmieniono kluczowych klatek ani opcji animacji. Kod workletu może być dowolnie złożony i umożliwia zdefiniowanie za pomocą kodu, które efekty mają być odtwarzane w jakiej kolejności i w jakim zakresie.

Opcje w opcjach

Możesz ponownie użyć workleta i zmienić jego numery. Dlatego konstruktor WorkletAnimation umożliwia przekazanie obiektu opcji do workletu:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

W tym przykładzie obie animacje są obsługiwane za pomocą tego samego kodu, ale z różnymi opcjami.

Powiedz mi, jak jest u Ciebie w kraju.

Jak już wspomniałem wcześniej, jednym z kluczowych problemów, które rozwiązuje animacja worklet, są animacje zależne od stanu. Elementy animacji mogą przechowywać stan. Jedną z podstawowych funkcji modułów roboczych jest jednak to, że można je przenieść do innego wątku lub nawet zniszczyć, aby zaoszczędzić zasoby. Aby zapobiec utracie stanu, moduł animacji udostępnia element wywoływania, który jest wywoływany przed usunięciem modułu, i którego można użyć do zwrócenia obiektu stanu. Ten obiekt zostanie przekazany konstruktorowi podczas ponownego tworzenia modułu. Po utworzeniu parametr ten będzie miał wartość undefined.

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

Za każdym razem, gdy odświeżysz to demo, masz 50% szans, że kwadrat zacznie się obracać w jedną lub drugą stronę. Jeśli przeglądarka rozłożyłaby worklet i przeniosła go do innego wątku, podczas tworzenia nastąpiłoby kolejne wywołanie Math.random(), co mogłoby spowodować nagłą zmianę kierunku. Aby temu zapobiec, zwracamy state jako kierunek losowo wybranej animacji i używamy go w konstruktorze, jeśli jest podany.

Łączenie z kontynentem czasoprzestrzennym: ScrollTimeline

Jak pokazano w poprzedniej sekcji, AnimationWorklet pozwala nam definiować za pomocą kodu, jak przesuwanie osi czasu wpływa na efekty animacji. Do tej pory nasz harmonogram był zawsze document.timeline, co oznacza, że śledzi czas.

ScrollTimeline otwiera nowe możliwości i pozwala sterować animacjami za pomocą przewijania zamiast czasu. Na potrzeby tego demo użyjemy naszego pierwszego workletu „passthrough”:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

Zamiast przekazywania wartości document.timeline tworzymy nową zmienną ScrollTimeline. Jak zapewne się domyślasz, ScrollTimeline nie używa czasu, ale pozycji scrollSource w układance, aby ustawić currentTime. Przewinięcie do góry (lub w lewo) oznacza currentTime = 0, a przewinięcie do dołu (lub w prawo) ustawia currentTime na timeRange. Jeśli przewiniesz to demo, możesz kontrolować położenie czerwonego pola.

Jeśli utworzysz ScrollTimeline z elementem, który nie przewija się, currentTime osi czasu będzie NaN. Dlatego, zwłaszcza w przypadku stron responsywnych, zawsze musisz mieć na uwadze, że NaN może być Twoim currentTime. Często warto ustawić domyślną wartość 0.

Połączenie animacji z pozycją przewijania było od dawna poszukiwane, ale nigdy nie udało się osiągnąć takiego poziomu wierności (poza niechlujnymi obejściami z CSS3D). Element animacji umożliwia proste wdrażanie tych efektów przy zachowaniu wysokiej wydajności. Na przykład: efekt paralaksy w przypadku przewijania, jak w tym demo, pokazuje, że definiowanie animacji opartej na przewijaniu zajmuje teraz tylko kilka linii kodu.

Na zapleczu

Worklety

Worklety to konteksty JavaScriptu z ograniczonym zakresem i bardzo małą powierzchnią interfejsu API. Mały interfejs API umożliwia bardziej agresywną optymalizację przez przeglądarkę, zwłaszcza na urządzeniach niższych klas. Co więcej, nie są one powiązane z konkretną pętlą zdarzeń, ale w razie potrzeby można je przenosić między wątkami. Jest to szczególnie ważne w przypadku AnimationWorklet.

Compositor NSync

Pewnie wiesz, że niektóre właściwości CSS są łatwe do animowania, a inne nie. Niektóre właściwości wymagają tylko pracy na karcie graficznej, aby można było je animować, a inne zmuszają przeglądarkę do ponownego ułożenia całego dokumentu.

W Chrome (i w wielu innych przeglądarkach) mamy proces zwany kompozytorem, którego zadaniem jest – i bardzo to upraszczam – układanie warstw i tekstur, a potem wykorzystanie procesora graficznego do aktualizowania ekranu tak regularnie, jak to możliwe, najlepiej z największą możliwą częstotliwością (zazwyczaj 60 Hz). W zależności od tego, które właściwości CSS są animowane, przeglądarka może potrzebować tylko kompozytora, podczas gdy inne właściwości muszą wykonywać układ, co jest operacją, którą może wykonać tylko główny wątek. W zależności od tego, które właściwości zamierzasz animować, moduł animacji może być powiązany z głównym wątkiem lub działać w osobnym wątku zsynchronizowanym z kompozytorem.

Uderzenie w nadgarstek

Zwykle jest tylko jeden proces kompozytora, który może być współdzielony na wielu kartach, ponieważ GPU jest zasobem o wysokiej konkurencji. Jeśli kompozytor zostanie w jakiś sposób zablokowany, cała przeglądarka przestanie działać i nie będzie reagować na polecenia użytkownika. Należy tego unikać za wszelką cenę. Co się stanie, jeśli worklet nie może dostarczyć danych potrzebnych do skompilowania ramki w czasie?

W takim przypadku zgodnie ze specyfikacją można „przesunąć” worklet. Zaczyna ona się opóźniać w stosunku do kompozytora, który może ponownie użyć danych z ostatniej klatki, aby utrzymać wysoką częstotliwość wyświetlania klatek. Wizualnie będzie to wyglądać jak twarde przewijanie, ale duża różnica polega na tym, że przeglądarka nadal reaguje na działania użytkownika.

Podsumowanie

AnimationWorklet ma wiele zalet i może być wykorzystywany na wiele sposobów. Oczywistymi zaletami są większa kontrola nad animacjami i nowe sposoby tworzenia animacji, które zapewniają wyższą jakość wizualną w internecie. Dzięki temu interfejsowi API możesz też zwiększyć odporność aplikacji na zacinanie, a zarazem uzyskać dostęp do wszystkich nowych funkcji.

Worklet animacji jest dostępny w Canary, a naszym celem jest przeprowadzenie testów Origin w Chrome 71. Nie możemy się doczekać, aż zaczniesz korzystać z nowych funkcji i dasz nam znać, co możemy jeszcze ulepszyć. Dostępny jest też polyfill, który zapewnia ten sam interfejs API, ale nie zapewnia izolacji pod względem wydajności.

Pamiętaj, że przejścia i animacje CSS są nadal prawidłowymi opcjami i w przypadku podstawowych animacji mogą być znacznie prostsze. Jeśli jednak chcesz stworzyć coś bardziej skomplikowanego, możesz skorzystać z AnimationWorklet.