Moduł7 - Zajęcia 14 - Throttle Debounce Lazyload

Throttle i Debounce

Dość często musimy poradzić sobie ze zmianą rozmiaru okna, przewijaniem, ruchem myszy lub wprowadzaniem tekstu przez użytkownika. Obsługa tych zdarzeń może polegać na sortowaniu kolekcji i renderowanie wyników, animowaniu elementu, manipulowania drzewem DOM i tak dalej. Wszystko to poprawia UX (user experience), ale niestety bardzo obciąża przeglądarkę ze względu na to, że funkcje do obsługi tych zdarzeń są uruchamiane zbyt często. Takie wydarzenia są nieformalnie nazywane «chatty events», ponieważ są właśnie "gadatliwe".

Dla przykładu, jeśli do przewijania strony dodasz słuchacza zdarzeń, to podczas przewijania strony rolką myszki / touchpadem możesz wywołać około 30 zdarzeń na sekundę. Powolne przewijanie (swipe) w smartfonie może wywołać do 100 zdarzeń na sekundę. Jeśli procedura obsługi zdarzeń przewijania wykonuje intensywne obliczenia i inne manipulacje DOM, wystąpią wtedy problemy z wydajnością. Nawet pozornie "lekkie" funkcje będą katastrofalnie powolne, jeśli wywołamy je 100 razy niemalże naraz.

Przykład -------------------------------

JavaScript:
const output = document.querySelector(".output");
let scrollEventCounter = 0;

document.addEventListener("scroll", () => {
  scrollEventCounter += 1;
  output.textContent = scrollEventCounter;
});
            

Scroll me

Number of scroll events

0

PrzykładEND -------------------------------

Throttle i Debounce to dwa podobne podejścia, ale różnią się nieco szczegółami swojego zachowania. Throttle jak i debounce pozwolą nam kontrolować, ile razy wykona się funkcja w danym czasie. Będziemy korzystać z ich implementacji w bibliotece Lodash.

Podłączanie biblioteki

CDN (Content Delivery Network) to rozproszona geograficznie infrastruktura sieciowa, która zapewnia szybkie dostarczanie treści użytkownikom usług i witryn internetowych. Serwery wchodzące w skład CDN są rozmieszczone geograficznie w taki sposób, aby zminimalizować czas odpowiedzi użytkowników serwisu/usługi.

Dodatkowo, jeśli użytkownik trafił wcześniej na inną stronę korzystającą z tego samego linka do danej biblioteki, to ma już załadowany plik w pamięci przeglądarki i nasza strona załaduje się jeszcze szybciej - w przypadku popularnych bibliotek zdarza się to całkiem często.

Podłączmy więc bibliotekę Lodash do projektu przez CDN. W tym celu skorzystajmy z usługi cdnjs.com i dodajmy link do skryptu biblioteki na końcu dokumentu HTML, jak pokazano w przykładzie.

  script async src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"
  integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ=="
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
  >/script>

Możemy teraz uzyskać dostęp do biblioteki w naszym skrypcie. Przy łączeniu bibliotek przez CDN, do obiektu window dodawana jest właściwość, która przechowuje to, co zapewnia biblioteka. Nazwa tej właściwości jest unikalna dla biblioteki i jest opisana w jej dokumentacji. Dla Lodash jest to znak podkreślenia _. Do sprawdzenia czy biblioteka jest poprawnie załadowana, używamy prostej metody add, która dodaje do siebie dwie liczby.

            const result = _.add(2, 3);
            console.log(result); // 5
          

Throttle

Technika throttle kontroluje, ile razy funkcja może być wywołana w określonym czasie. Oznacza to, że pozwala na wywołanie funkcji nie więcej niż raz w ciągu N milisekund, zapewniając jej regularne i niezbyt częste wykonywanie.

Używając throttle nie mamy kontroli nad tym, jak często przeglądarka uruchamia zdarzenia. Po prostu przejmujemy kontrolę nad tym, jak często wykonywana jest funkcja obsługi zdarzeń, niezależnie od tego ile wywołań się pojawi.

            document.addEventListener(
              "scroll",
              _.throttle(() => {
                console.log("Scroll handler call every 300ms");
              }, 300)
            );
          

Implementacja z biblioteki Lodash oczekuje, że funkcja będzie przekazana jako pierwszy argument, a ilość milisekund odstępu jako drugi. Zwraca nową funkcję, którą możemy przekazać do nasłuchiwacza zdarzeń.

Przykład -------------------------------

const vanillaOutput = document.querySelector('.output-s1a3.vanilla-s1a3');
const throttledOutput = document.querySelector('.output-s1a3.throttled-s1a3');
const eventCounter = {
  vanilla: 0,
  throttled: 0,
};

document.addEventListener('scroll', () => {
  eventCounter.vanilla += 1;
  vanillaOutput.textContent = eventCounter.vanilla;
});

document.addEventListener(
  'scroll',
  _.throttle(() => {
    eventCounter.throttled += 1;
    throttledOutput.textContent = eventCounter.throttled;
  }, 300)
);
            

Scroll me

No timing function

0

Throttled

0

PrzykładEND -------------------------------

Debounce

Technika debounce zapewnia, że funkcja zostanie wywołana tylko wtedy, gdy między zdarzeniami nastąpi przerwa wynosząca N milisekund. W poniższym przykładzie, gdy użytkownik przewija stronę, funkcja nie zostanie wywołana, ale gdy tylko przestanie przewijać, funkcja zostanie wywołana po 300 milisekundach przerwy od ostatniego wywołania funkcji. Jeśli przewijanie zostanie wznowione wcześniej niż 300 milisekund od poprzedniego wywołania, funkcja nie zostanie wywołana.

Używając debounce nie kontrolujemy, jak często przeglądarka będzie generować zdarzenia, a jedynie przejmujemy kontrolę nad częstotliwością wykonywania funkcji obsługi zdarzeń.

            document.addEventListener(
              "scroll",
              _.debounce(() => {
                console.log("Scroll handler call after 300ms pause");
              }, 300)
            );
          

Implementacja z biblioteki Lodash oczekuje funkcji jako pierwszego argumentu i liczby milisekund jako drugiego. Zwraca nową funkcję, którą możemy przekazać do nasłuchiwacza zdarzeń.

Przykład -------------------------------

const vanillaOutput_s1a4 = document.querySelector('.output-s1a4.vanilla-s1a4');
const throttledOutput_s1a4 = document.querySelector(
  '.output-s1a4.throttled-s1a4'
);
const debouncedOutput_s1a4 = document.querySelector(
  '.output-s1a4.debounced-s1a4'
);
const eventCounter_s1a4 = {
  vanilla: 0,
  throttled: 0,
  debounced: 0,
};

document.addEventListener('scroll', () => {
  eventCounter_s1a4.vanilla += 1;
  vanillaOutput_s1a4.textContent = eventCounter.vanilla;
});

document.addEventListener(
  'scroll',
  _.throttle(() => {
    eventCounter_s1a4.throttled += 1;
    throttledOutput_s1a4.textContent = eventCounter_s1a4.throttled;
  }, 300)
);

document.addEventListener(
  'scroll',
  _.debounce(() => {
    eventCounter_s1a4.debounced += 1;
    debouncedOutput_s1a4.textContent = eventCounter_s1a4.debounced;
  }, 300)
);
            

Scroll me

No timing function

0

Throttled

0

Debounced

0

PrzykładEND -------------------------------

Tryby metody debounce

Domyślnie metoda debounce działa w trybie, w którym funkcja jest wywoływana w ciągu N milisekund po przerwie między "strumieniami" zdarzeń. Ten tryb nazywa się trailing edge (na końcu).

Są jednak zadania, w których funkcję trzeba wywołać natychmiast po wystąpieniu pierwszego zdarzenia w strumieniu, a następnie zignorować wszystkie kolejne zdarzenia, aż do odpowiednio długiej przerwy między nimi, na przykład 300 milisekund. To zachowanie jest powtarzane na początku następnego strumienia zdarzeń. Ten tryb nazywa się leading edge (na początku).

Do metody debounce biblioteki Lodash można przekazać opcjonalny trzeci argument — obiekt parametru, który ma dwie właściwości leading (domyślnie false) i trailing (domyślnie true). Ustawienia te zmieniają tryb i wskazują, czy funkcja ma działać na początku strumienia zdarzeń, czy na końcu po przerwie.

            document.addEventListener(
              "scroll",
              _.debounce(
              () => {
                console.log("Scroll handler call on every event stream start");
              },
              300,
              {
                leading: true,
                trailing: false,
              }
              )
              );
          

W praktyce tryb leading może być wykorzystany np. wtedy, gdy konieczne jest wykonanie funkcji wysłania żądania do serwera przy pierwszym kliknięciu przycisku, a następnie ignorowanie wszystkich kolejnych kliknięć aż do odpowiednio długiej pauzy na przykład w celu uniknięcia odruchowe dwukliku użytkownika. Przykład implementuje debounce w obu trybach dla zdarzenia scroll.

Przykład -------------------------------

const vanillaOutput_s1a5 = document.querySelector('.output-s1a5.vanilla-s1a5');
const throttledOutput_s1a5 = document.querySelector(
  '.output-s1a5.throttled-s1a5'
);
const trailingOutput_s1a5 = document.querySelector(
  '.output-s1a5.trailing-s1a5'
);
const leadingOutput_s1a5 = document.querySelector('.output-s1a5.leading-s1a5');
const eventCounter_s1a5 = {
  vanilla: 0,
  throttled: 0,
  trailing: 0,
  leading: 0,
};

// Trailing debounce
document.addEventListener(
  'scroll',
  _.debounce(() => {
    eventCounter_s1a5.trailing += 1;
    trailingOutput_s1a5.textContent = eventCounter_s1a5.trailing;
  }, 300)
);

// Leading debounce
document.addEventListener(
  'scroll',
  _.debounce(
    () => {
      eventCounter_s1a5.leading += 1;
      leadingOutput_s1a5.textContent = eventCounter_s1a5.leading;
    },
    300,
    { trailing: false, leading: true }
  )
);

document.addEventListener('scroll', () => {
  eventCounter_s1a5.vanilla += 1;
  vanillaOutput_s1a5.textContent = eventCounter_s1a5.vanilla;
});

document.addEventListener(
  'scroll',
  _.throttle(() => {
    eventCounter_s1a5.throttled += 1;
    throttledOutput_s1a5.textContent = eventCounter_s1a5.throttled;
  }, 300)
);
            

Scroll me

No timing function

0

Throttled

0

Trailing debounce

0

Leading debounce

0

PrzykładEND -------------------------------

Leniwe ładowanie

Strony internetowe zawierają często dużą liczbę obrazów, które zwiększają rozmiar stron i dramatycznie wpływają na szybkość ich ładowania. Większość obrazów znajduje się poza pierwszą widoczną sekcją (below the fold), więc użytkownik zobaczy je dopiero po przewinięciu strony w dół. Oznacza to, że ładuje się coś, czego użytkownik może nigdy nie zobaczyć. Poświęcisz więc na to czas ładowania strony a często stracisz uwagę użytkowników i pozycję w wyszukiwarkach, ponieważ jedni i drudzy wybierają chętniej szybko ładujące się strony. Pobieranie niekrytycznych treści powoduje również marnowanie baterii urządzeń mobilnych, ograniczonego transferu danych komórkowych i innych zasobów systemowych.

Terminy "above the fold" i "below the fold" pochodzą z czasów "offline". Jeśli kiedykolwiek kupiłeś gazetę w kiosku, zwykle była złożona ona na pół, aby przechodnie mogli zobaczyć tylko górną połowę pierwszej strony. Jeśli nie spodoba im się to, co widzą, przejdą obok i sprzedaż spadnie. Podobnie w dzisiejszych czasach ważne jest, aby najciekawsze treści umieszczać na górze strony i ładować je maksymalnie szybko.

Leniwe ładowanie (lazy-loading) to technika, która opóźnia ładowanie niekrytycznych zasobów podczas ładowania strony. Zamiast tego te niekrytyczne zasoby są ładowane tylko wtedy, gdy są potrzebne. Zmniejsza to początkowy rozmiar zasobów, które należy załadować, aby wyświetlić stronę. Zużycie zasobów systemowych oraz czas jej ładowania i późniejszego renderowania są w taki sposób mniejsze. Wszystko to ma pozytywny wpływ na wydajność.

Prawdopodobnie widziałeś już leniwe ładowanie w akcji. Wygląda to mniej więcej tak:

  • Wchodzisz na stronę i zaczynasz ją przewijać czytając treść.
  • W pewnym momencie przewiniesz odpowiednio daleko w dół i zobaczysz "zaślepkę" czyli pusty zastępczy obraz, lub wersję w niewielkiej rozdzielczości.
  • Zaślepka zostaje nagle zastąpiona prawdziwym obrazem lub jego pełnowymiarową wersją.

Atrybut loading

Wcześniej programiści musieli polegać tylko na możliwościach JavaScript. Nowoczesne przeglądarki potrafią to zrobić bez JavaScript, ale niestety nie jest wspierane to jeszcze we wszystkich z nich. Atrybut HTML loading znacznika "img" jest obsługiwany natywnie we wszystkich nowoczesnych przeglądarkach z wyjątkiem Safari (choć obsługa pojawia się w jej wersji beta co pozwala mieć nadzieję na wprowadzenie kompatybilności) i pozwala przeglądarce opóźniać ładowanie obrazów poza ekranem, dopóki użytkownik nie przewinie tak, że pojawią się w jego widoku (viewport).

            img src="my-image.jpg" loading="lazy" alt="Image description" />
          

Atrybut obsługuje trzy wartości:

  • lazy - przeglądarka wykona leniwe ładowanie obrazu.
  • eager - obraz zostanie załadowany tak szybko, jak to możliwe, czyli bez leniwego ładowania.
  • auto - przeglądarka sama określa, czy wykonać leniwe ładowanie, czy nie, na bazie informacji które posiada między innymi o preferencjach użytkownika, jakości połączenia i tak dalej. Domyślna wartość.

Nie możemy rozpoznać ani zmienić zachowania i mechanizmu określania leniwego ładowania obrazu przez przeglądarkę. Najważniejsze jest to, że przeglądarka załaduje takie obrazy na krótko przed ich wejściem do pola widoku.

Otwórz kartę Network w narzędziach programistycznych i wybierz filtr Img, aby wyświetlić tylko ładowanie obrazów. Następnie przewiń przykład i zobacz, jak ładowane są obrazy kotów znajdujące się poza widokiem. Przeglądarki obsługujące atrybut loading ładują obrazy z opóźnieniem, podczas gdy przeglądarki bez obsługi ładują wszystkie obrazy jednocześnie.

Biblioteka lazysizes

Aby zapewnić wysoką kompatybilność, czyli wsparcie dla starszych przeglądarek lub takich, które nie obsługują jeszcze atrybutu loading natywnie, możesz użyć kilku istniejących bibliotek JavaScript. Najpopularniejsze to lazysizes, vanilla-lazyload i lozad.js. Wybór biblioteki sprowadza się do tego, jaki zestaw dostarczanych funkcji oferuje i osobistych preferencji programisty. Przyjrzymy się bibliotece lazysizes.

Obsługa natywna jest ogólnie lepsza i bardziej wydajna niż korzystanie z bibliotek, ale one gwarantują działanie we wszystkich przeglądarkach i mogą zapewnić zaawansowane możliwości leniwego ładowania, które nie są jeszcze ustandaryzowane.

Pierwszą rzeczą do zrobienia jest podłączenie biblioteki do projektu za pomocą usługi cdnjs.com. Tag z linkiem do skryptu jest dodawany na końcu "body", tak jak zrobiliśmy to dla biblioteki Lodash.

            !-- Lazysizes library script file --
  script
    src="https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.2/lazysizes.min.js"
    integrity="sha512-q583ppKrCRc7N5O0n2nzUiJ+suUv7Et1JGels4bXOaMFQcamPk9HjdUknZuuFjBNs7tsMuadge5k9RzdmO+1GQ=="
    crossorigin="anonymous"
    referrerpolicy="no-referrer"
  >/script>
          

Biblioteka lazysizes jest inicjowana samoczynnie po załadowaniu na stronę. Oznacza to, że dla podstawowego użycia, w JavaScript nie musisz nic robić. Pełna lista możliwości biblioteki znajduje się w dokumentacji.

Biblioteka lazysizes jest inicjowana samoczynnie po załadowaniu na stronę. Oznacza to, że dla podstawowego użycia, w JavaScript nie musisz nic robić. Pełna lista możliwości biblioteki znajduje się w dokumentacji.

Dla wszystkich obrazów, które mają być ładowane leniwie, ustawiamy klasę lazyload i zastępujemy atrybut src atrybutem data-src. Biblioteka lazysizes potrzebuje tego do poprawnego działania.

            img class="lazyload" data-src="path/to/my-image.jpg" alt="Generic alt" />
          

Podczas wczytywania obrazu możesz wyświetlić symbol zastępczy niskiej jakości. Ta technika nazywa się LQIP (Low Quality Image Placeholder). Istnieje wiele opcji implementacji LQIP, ale na początek wystarczy pokazać jeden standardowy symbol zastępczy zamiast wszystkich obrazów. Aby to zrobić, dodaj atrybut src, którego wartością będzie za każdym razem link do tego samego obrazu zastępczego.

            img
            class="lazyload"
            src="path/to/lqip-placeholder.jpg"
            data-src="path/to/my-image.jpg"
            alt="Generic alt"
            />
          

Po pełnym załadowaniu obrazu biblioteka lazysizes dodaje do elementu klasę lazyloaded. Można to wykorzystać do zastosowania efektów CSS w momencie ładowania obrazu.

            .blur-up {
              filter: blur(5px);
              transition: filter 400ms;
            }

            .blur-up.lazyloaded {
              filter: blur(0);
            }
          

Po zadeklarowaniu stylów dodaj klasę blur-up do znaczników "img".

            img
            class="lazyload blur-up"
            src="path/to/lqip-placeholder.jpg"
            data-src="path/to/my-image.jpg"
            alt="Generic alt"
            />
          

Zastosujmy wszystkie te kroki na przykładzie, dodając wsparcie leniwego ładowania obrazów na naszej stronie o kotach. Teraz nawet Safari leniwie załaduje obrazy.