15 maj 2019

IntersectionObserver czyli lazy loading

IntersectionObserver - w prosty sposób można opisać że jest to API za pomocą którego można wywołać zdarzenie, gdy element zniknie lub pojawi się w obrębie naszego ekranu, a jeszcze prościej na przykładzie np. galerii zdjęć. Wczytać tylko te zdjęcia które pojawią się w polu widzenia naszego okna.

Czyli nie ładujemy powiedzmy 100 zdjęć tylko doczytujemy pojedyncze zdjęcia przy skrolowaniu. Do tej pory wykorzystywaliśmy pluginy które nam to ułatwiały.

IntersectionObserver można wykorzystać nie tylko do wczytywania zdjęć, ale np. filmów, treści wszystko to dzieje się asynchronicznie.

Np. film włączony na stronie jeżeli zaczniemy przewijać i zniknie nam z "widoku" zostanie zastopowane odtwarzanie. Jeżeli ponownie się pojawi zostaje wznowione jego odtwarzanie.

Niestety jest jeden problem, w związku z tym że jest to nowa technologia nie jest obsługiwana przez starsze przeglądarki. Zerknijcie na Caniuse Sami sprawdźcie.

Żyjemy w czasach polyfill i na to również jest, działa nawet w IE7 😉 github

Dobra zaczynamy, najpierw mamy listę zwykłych obrazków a do tego możemy również wykorzystać picture

<img width="400" height="400" data-src="https://place-hold.it/400x400/15252D/fff">
<img width="400" height="400" data-src="https://place-hold.it/400x400/15252D/fff">
<img width="400" height="400" data-src="https://place-hold.it/400x400/15252D/fff">
<img width="400" height="400" data-src="https://place-hold.it/400x400/15252D/fff">
...

Picture

<picture>
  <source media="(min-width: 400px)" data-srcset="https://place-hold.it/400x400/15252D/fff">
  <source media="(min-width: 200px)" data-srcset="https://place-hold.it/200x200/15252D/fff">
  <img data-src="https://place-hold.it/100x100/15252D/fff">
</picture>

<picture>
  <source media="(min-width: 400px)" data-srcset="https://place-hold.it/400x400/15252D/fff">
  <source media="(min-width: 200px)" data-srcset="https://place-hold.it/200x200/15252D/fff">
  <img data-src="https://place-hold.it/100x100/15252D/fff">
</picture>

<picture>
  <source media="(min-width: 400px)" data-srcset="https://place-hold.it/400x400/15252D/fff">
  <source media="(min-width: 200px)" data-srcset="https://place-hold.it/200x200/15252D/fff">
  <img data-src="https://place-hold.it/100x100/15252D/fff">
</picture>

<picture>
  <source media="(min-width: 400px)" data-srcset="https://place-hold.it/400x400/15252D/fff">
  <source media="(min-width: 200px)" data-srcset="https://place-hold.it/200x200/15252D/fff">
  <img data-src="https://place-hold.it/100x100/15252D/fff">
</picture>

Jak widać w powyższym html nie ma nic szczególnego.
Teraz czas na javascript.

let images = document.querySelectorAll('source, img');

if ('IntersectionObserver' in window) {
  // sprawdzamy czy IntersectionObserver jest obsługiwany 
  // przez nasze przeglądarki
  let config = {
    root: null,
    rootMargin: '0px',
    threshold: 0.5
  };

  let observer = new IntersectionObserver(onChange, config);
  images.forEach(function (img) { observer.observe(img) });

  function onChange(changes, observer) {
    changes.forEach(function (change) {
      if (change.intersectionRatio > 0) {
        // przestajemy obserwować i ładować obrazki
        loadImage(change.target);
        observer.unobserve(change.target);
      }
    });
  }

} else {
  // jeżeli IntersectionObserver nie jest obsługiwany
  // to ładujemy wszystkie zdjęcia
  images.forEach(function (image) { loadImage(image) });
}

function loadImage(image) {
  image.classList.add('fade-in');
  if (image.dataset && image.dataset.src) {
    image.src = image.dataset.src;
  }

  if (image.dataset && image.dataset.srcset) {
    image.srcset = image.dataset.srcset;
  }
}

let images - pobieramy wszystkie elementy source oraz img
Później sprawdzamy czy IntersectionObserver jest obsługiwany w naszej przeglądarce.

Następnie konfiguracja składająca się z root, rootMargin, threshold.

root - jest to element np. jakiś div za pomocą którego sprawdzamy widoczność celu. Domyślnie jest to browser viewport. Jeżeli jest on nie ustawiony musi być podany jako null
rootMargin - jest to margines wokół głównego elementu "root". Ma takie same właściwości marginesu jak w CSS czyli (góra, prawo, dół, lewo). Właściwości mogą być również podane w procentach. Ten element służy do powiększania lub zmniejszania "bounding box" głównego elementu. Domyślnie są to wszystkie zera.
Ostatni element threshold chyba najważniejszy element. Jest to pojedyncza liczba lub tablica liczba, które wskazują w jakim procencie widoczności powinien być wykonany "callback". Np. jeżeli chcemy wykryć kiedy widoczność przekroczy 50% ustawiamy 0.5. Jeśli chcemy aby wywołanie zwrotne było uruchamiane za każdym razem gdy widoczność przekroczy 25% należy podać tablicę [0, 0.25, 0.5, 0.75, 1]. Wartość domyślna to 0 oznacza to że gdy tylko jeden piksel jest widoczny callback zostanie uruchomiony.

Za pomocą pomocniczej funkcji "loadImage" zamieniamy data-src i data-srcset na src.

Oczywiście problemem jest to, że nie wszędzie ten kod zadziała, bo nie wszystkie przeglądarki obsługują to api.
Rozwiązaniem jest "images.forEach(function (image) { loadImage(image) });" zwyczajnie pomijamy te cuda z api i ładujemy wszystkie obrazki naraz.

Jednak jeżeli chcemy aby IntersectionObserver był obsługiwany przez większą ilość przeglądarek należy dodać polyfill. Na wstępie podałem link do takowego.
Oczywiście za pomocą poniższego przykładu możemy dodać tyle polyfills ile nam potrzeba 😉

const modernBrowser = ('IntersectionObserver' in window);

if (!modernBrowser) {
  loadScripts([
    "./polyfills/intersection-observer.js"
  ])
}

function loadScripts(array, callback) {
  var loader = function (src, handler) {
    var script = document.createElement("script");
    script.src = src;
    script.onload = script.onreadystatechange = function () {
      script.onreadystatechange = script.onload = null;
      handler();
    }
    var head = document.getElementsByTagName("head")[0];
    (head || document.body).appendChild(script);
  };
  (function run() {
    if (array.length != 0) {
      loader(array.shift(), run);
    } else {
      callback && callback();
    }
  })();
}

Możemy to jeszcze prościej zrobić skorzystać ze strony polyfill.io, poniższy kawałek kodu wklejamy w html.

<script type="text/javascript">
  if (!('IntersectionObserver' in window)) {
     var script = document.createElement("script");
     script.src = "https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver";
     document.getElementsByTagName('head')[0].appendChild(script);
  }
</script>

I to tyle, poniżej działający nasz przykład niestety jsfiddle dziwnie wyświetla element picture hm.

UWAGA!

Niestety takie rozwiązania mają jeden ale za to bardzo poważny problem, a jest nim SEO. Niestety google nie indeksuje takich zdjęć.
Przetestowałem rozwiązanie, które rozwiązuje ten problem a mianowicie jest dodanie noscript

<noscript><img decoding="async" src="https://place-hold.it/100x100/15252D/fff"></noscript>

a całość powinna tak wyglądać:

<picture>
  <source media="(min-width: 400px)" data-srcset="https://place-hold.it/400x400/15252D/fff">
  <source media="(min-width: 200px)" data-srcset="https://place-hold.it/200x200/15252D/fff">
  <img data-src="https://place-hold.it/100x100/15252D/fff">
  <noscript><img decoding="async" src="https://place-hold.it/100x100/15252D/fff"></noscript>
</picture>

lub jeżeli używamy w normalny sposób zdjęcie:

<img width="400" height="400" data-src="https://place-hold.it/400x400/15252D/fff">
<noscript><img decoding="async" width="400" height="400" src="https://place-hold.it/400x400/15252D/fff"></noscript>

Takie rozwiązanie pozwala nam zachować bardzo dobrze zoptymalizowaną stronę a zarazem pozwala na dostęp do zdjęć robotom google.

Natywne ładowanie obrazu w Internecie!

Od wersji chrome 75 przeglądarka obsługuje natywnie "lazy loading"

<img decoding="async" src="celebration.jpg" loading="lazy" alt="..." />
<iframe src="video-player.html" loading="lazy"></iframe>

Więcej możecie dowiedzieć się z tej strony