Teraz mnie widzisz: jak odroczyć, leniwie ładować i działać za pomocą IntersectionObserver

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Informacje o skrzyżowaniach są potrzebne z wielu powodów, takich jak leniwe ładowanie obrazów. Ale jest o wiele więcej. Nadszedł czas, aby lepiej zrozumieć i z różnych perspektyw korzystać z interfejsu API Intersection Observer. Gotowy?

Dawno, dawno temu żył twórca stron internetowych, który skutecznie przekonał swoich klientów, że witryny nie powinny wyglądać tak samo we wszystkich przeglądarkach, dbał o dostępność i był jednym z pierwszych użytkowników siatek CSS. Ale w głębi serca to performance był jego prawdziwą pasją: stale optymalizował, minimalizował, monitorował, a nawet stosował psychologiczne triki w swoich projektach.

Pewnego dnia dowiedział się o leniwym wczytywaniu się obrazów i innych zasobów, które nie są od razu widoczne dla użytkowników i nie są niezbędne do renderowania treści na ekranie. To był początek świtu: deweloper wkroczył w zły świat leniwie ładujących się wtyczek jQuery (a może nie tak zły świat atrybutów async i defer ). Niektórzy twierdzą nawet, że trafił prosto w sedno całego zła: świat słuchaczy zdarzeń scroll . Nigdy nie dowiemy się na pewno, gdzie trafił, ale z drugiej strony ten programista jest absolutnie fikcyjny, a jakiekolwiek podobieństwo do jakiegokolwiek programisty jest po prostu przypadkowe.

programista stron internetowych
Fikcyjny web developer

Cóż, teraz można powiedzieć, że puszka Pandory została otwarta i że nasz fikcyjny programista nie umniejsza tego problemu. W dzisiejszych czasach priorytetyzacja treści w części strony widocznej na ekranie stała się niezwykle ważna dla wydajności naszych projektów internetowych, zarówno z punktu widzenia szybkości, jak i wagi strony.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

W tym artykule wyjdziemy z ciemności scroll i porozmawiamy o nowoczesnym sposobie leniwego ładowania zasobów. Nie tylko leniwe ładowanie obrazów, ale także wczytywanie dowolnego zasobu. Co więcej, technika, o której będziemy dzisiaj mówić, jest w stanie znacznie więcej niż tylko leniwe ładowanie zasobów: będziemy w stanie zapewnić dowolny rodzaj odroczonej funkcjonalności w oparciu o widoczność elementów dla użytkowników.

IntersectionObserver: Teraz mnie widzisz

Panie i panowie, porozmawiajmy o API Intersection Observer. Ale zanim zaczniemy, spójrzmy na krajobraz nowoczesnych narzędzi, który doprowadził nas do IntersectionObserver .

Rok 2017 był bardzo dobrym rokiem dla narzędzi wbudowanych w nasze przeglądarki, które pomogły nam bez większego wysiłku poprawić jakość i styl naszego kodu. W dzisiejszych czasach wydaje się, że sieć odchodzi od sporadycznych rozwiązań opartych na bardzo różnych rozwiązaniach, niż rozwiązywanie bardzo typowe dla bardziej dobrze zdefiniowanego podejścia do interfejsów Observera (lub po prostu „Observers”): Dobrze obsługiwany MutationObserver zyskał nowych członków rodziny, którzy zostali szybko przyjęte w nowoczesnych przeglądarkach:

  • Obserwator skrzyżowania i
  • PerformanceObserver (jako część specyfikacji Performance Timeline Level 2).

Jeszcze jeden potencjalny członek rodziny, FetchObserver, jest w toku i prowadzi nas bardziej w krainy proxy sieci, ale dzisiaj chciałbym porozmawiać o front-endzie.

IntersectionObserver i PerformanceObserver to nowi członkowie rodziny Observers.
IntersectionObserver i PerformanceObserver to nowi członkowie rodziny Observers.

PerformanceObserver i IntersectionObserver mają na celu pomóc programistom front-end poprawić wydajność ich projektów w różnych punktach. Pierwsza z nich daje nam narzędzie do Realnego Monitorowania Użytkownika, druga zaś jest narzędziem, które zapewnia nam wymierną poprawę wydajności. Jak wspomniano wcześniej, w tym artykule przyjrzymy się dokładnie temu drugiemu: IntersectionObserver . Aby w szczególności zrozumieć mechanikę IntersectionObserver , powinniśmy przyjrzeć się, jak powinien działać ogólny Observer we współczesnej sieci.

Wskazówka dla profesjonalistów: możesz pominąć teorię i od razu zagłębić się w mechanikę IntersectionObserver lub, jeszcze dalej, bezpośrednio do możliwych zastosowań IntersectionObserver .

Obserwator kontra zdarzenie

„Obserwator”, jak sama nazwa wskazuje, ma za zadanie obserwować coś, co dzieje się w kontekście strony. Obserwatorzy mogą obserwować, co dzieje się na stronie, na przykład zmiany DOM. Mogą również obserwować zdarzenia cyklu życia strony. Obserwatorzy mogą również uruchomić niektóre funkcje zwrotne. Teraz uważny czytelnik może od razu zauważyć problem i zapytać: „Więc o co chodzi? Czy nie mamy już wydarzeń w tym celu? Co wyróżnia Obserwatorów?” Bardzo dobry punkt! Przyjrzyjmy się bliżej i rozwiążmy to.

Obserwator a wydarzenie: jaka jest różnica?
Obserwator a wydarzenie: jaka jest różnica?

Kluczowa różnica między zwykłym Zdarzeniem a Obserwatorem polega na tym, że domyślnie ten pierwszy reaguje synchronicznie na każde wystąpienie Zdarzenia, wpływając na reakcję głównego wątku, podczas gdy ten drugi powinien reagować asynchronicznie, nie wpływając tak bardzo na wydajność. Przynajmniej dotyczy to obecnie prezentowanych Obserwatorów: wszyscy zachowują się asynchronicznie i nie sądzę, aby miało to się zmienić w przyszłości.

Prowadzi to do głównej różnicy w obsłudze wywołań zwrotnych obserwatorów, która może zmylić początkujących: asynchroniczna natura obserwatorów może skutkować przekazaniem kilku obserwowalnych do funkcji wywołania zwrotnego w tym samym czasie. Z tego powodu funkcja wywołania zwrotnego powinna oczekiwać nie pojedynczego wpisu, ale Array wpisów (nawet jeśli czasami tablica zawiera tylko jeden wpis).

Co więcej, niektórzy Obserwatorzy (szczególnie ten, o którym mówimy dzisiaj) dostarczają bardzo przydatne, wstępnie obliczone właściwości, które w innym przypadku zwykliśmy obliczać za pomocą drogich (z punktu widzenia wydajności) metod i właściwości podczas korzystania z regularnych zdarzeń. Aby wyjaśnić tę kwestię, przejdziemy do przykładu w dalszej części artykułu.

Więc jeśli trudno jest komuś odejść od paradygmatu zdarzeń, powiedziałbym, że Obserwatorzy to zdarzenia na sterydach. Inny opis brzmiałby: Obserwatorzy to nowy poziom przybliżenia na szczycie wydarzeń. Ale bez względu na to, którą definicję wolisz, powinno być oczywiste, że Obserwatorzy nie mają na celu zastępowania zdarzeń (przynajmniej jeszcze nie); jest wystarczająco dużo przypadków użycia dla obu i mogą szczęśliwie żyć obok siebie.

Obserwatorzy nie mają na celu zastępowania wydarzeń: obaj mogą żyć razem szczęśliwie.
Obserwatorzy nie mają na celu zastępowania wydarzeń: obaj mogą żyć razem szczęśliwie.

Ogólna struktura obserwatora

Ogólna struktura obserwatora (dowolnego z dostępnych w chwili pisania tego tekstu) wygląda podobnie do tej:

 /** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);

Ponownie zauważ, że entriesArray wartości, a nie pojedynczym wpisem.

Oto ogólna struktura: implementacje poszczególnych Observerów różnią się argumentami przekazywanymi do funkcji observe() i argumentami przekazywanymi do funkcji zwrotnej. Na przykład MutationObserver powinien również otrzymać obiekt konfiguracyjny, aby dowiedzieć się więcej o tym, jakie zmiany w DOM należy obserwować. PerformanceObserver nie obserwuje węzłów w DOM, ale zamiast tego ma dedykowany zestaw typów wpisów, które może obserwować.

Tutaj zakończmy „ogólną” część tej dyskusji i zagłębmy się w temat dzisiejszego artykułu — IntersectionObserver .

Dekonstruowanie obserwatora skrzyżowania

Dekonstruowanie obserwatora skrzyżowania
Dekonstruowanie obserwatora skrzyżowania

Przede wszystkim ustalmy, czym jest IntersectionObserver .

Według MDN:

Interfejs API Intersection Observer umożliwia asynchroniczną obserwację zmian w przecięciu elementu docelowego z elementem przodka lub widokiem dokumentu najwyższego poziomu.

Mówiąc najprościej, IntersectionObserver asynchronicznie obserwuje nakładanie się jednego elementu na inny. Porozmawiajmy o tym, do czego służą te elementy w IntersectionObserver .

Inicjalizacja obserwatora skrzyżowania

W jednym z poprzednich akapitów widzieliśmy strukturę ogólnego Obserwatora. IntersectionObserver nieco rozszerza tę strukturę. Przede wszystkim tego typu Observer wymaga konfiguracji z trzema głównymi elementami:

  • root : jest to element główny używany do obserwacji. Definiuje podstawową „ramkę przechwytywania” dla obserwowalnych elementów. Domyślnie root jest widok przeglądarki, ale tak naprawdę może to być dowolny element w DOM (wtedy ustawiasz root na coś w rodzaju document.getElementById('your-element') ). Pamiętaj jednak, że elementy, które chcesz obserwować, muszą w tym przypadku „żyć” w drzewie DOM root .
własność roota konfiguracji IntersectionObserver
Własność root definiuje podstawę do „przechwytywania klatki” dla naszych elementów.
  • rootMargin : definiuje margines wokół elementu root , który rozszerza lub zmniejsza „ramkę przechwytywania”, gdy wymiary elementu root nie zapewniają wystarczającej elastyczności. Opcje wartości tej konfiguracji są podobne do wartości margin w CSS, na przykład rootMargin: '50px 20px 10px 40px' (góra, prawy dół, lewy). Wartości mogą być skrócone (jak rootMargin: '50px' ) i mogą być wyrażone w px lub % . Domyślnie rootMargin: '0px' .
Właściwość rootMargin konfiguracji IntersectionObserver
Właściwość rootMargin rozszerza/zawęża „ramkę przechwytywania”, która jest zdefiniowana przez root .
  • threshold : nie zawsze chce reagować natychmiast, gdy obserwowany element przecina granicę „ramki przechwytywania” (zdefiniowanej jako kombinacja root i rootMargin ). threshold określa procent takiego skrzyżowania, na którym powinien zareagować Obserwator. Może być zdefiniowany jako pojedyncza wartość lub jako tablica wartości. Aby lepiej zrozumieć wpływ threshold (wiem, że czasami może to być mylące), oto kilka przykładów:
    • threshold: 0 : Wartość domyślna IntersectionObserver powinna reagować, gdy pierwszy lub ostatni piksel obserwowanego elementu przecina jedną z granic „klatki przechwytywania”. Należy pamiętać, że IntersectionObserver jest niezależny od kierunku, co oznacza, że ​​zareaguje w obu scenariuszach: a) gdy element wejdzie i b) gdy opuści „ramkę przechwytywania”.
    • threshold: 0.5 : Obserwator powinien zostać uruchomiony, gdy 50% obserwowanego elementu przecina „klatkę przechwytywania”;
    • threshold: [0, 0.2, 0.5, 1] ​​: Obserwator powinien zareagować w 4 przypadkach:
      • Pierwszy piksel obserwowanego elementu wchodzi do „klatki przechwytywania”: element nadal tak naprawdę nie znajduje się w tej klatce lub ostatni piksel obserwowanego elementu opuszcza „ramkę przechwytywania”: element nie znajduje się już w ramce;
      • 20% elementu znajduje się w „ramce przechwytywania” (znowu kierunek nie ma znaczenia dla IntersectionObserver );
      • 50% elementu znajduje się w „ramce przechwytywania”;
      • 100% elementu znajduje się w „ramce przechwytywania”. Jest to dokładnie przeciwne do threshold: 0 .
właściwość progu konfiguracji IntersectionObserver
Właściwość threshold określa, o ile element powinien przecinać naszą „klatkę przechwytywania”, zanim Observer zostanie uruchomiony.

Aby poinformować naszego IntersectionObserver o pożądanej konfiguracji, po prostu przekazujemy nasz obiekt config do konstruktora naszego Observera wraz z naszą funkcją zwrotną w następujący sposób:

 const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);

Teraz powinniśmy podać IntersectionObserver rzeczywisty element do obserwowania. Odbywa się to po prostu przez przekazanie elementu do funkcji observe() :

 … const img = document.getElementById('image-to-observe'); observer.observe(image);

Kilka rzeczy do zapamiętania na temat tego obserwowanego elementu:

  • Wspomniano o tym wcześniej, ale warto wspomnieć jeszcze raz: W przypadku ustawienia root jako elementu w DOM, obserwowany element powinien znajdować się w drzewie DOM root .
  • IntersectionObserver może akceptować tylko jeden element do obserwacji na raz i nie obsługuje dostaw partii dla obserwacji. Oznacza to, że jeśli chcesz obserwować kilka elementów (powiedzmy kilka obrazków na stronie), musisz przejrzeć je wszystkie i obserwować każdy z nich z osobna:
 … const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
  • Podczas ładowania strony z zainstalowanym Observerem możesz zauważyć, że wywołanie zwrotne IntersectionObserver zostało uruchomione dla wszystkich obserwowanych elementów jednocześnie. Nawet te, które nie pasują do dostarczonej konfiguracji. „Cóż… nie do końca to, czego się spodziewałem”, to zwykła myśl, gdy doświadczam tego po raz pierwszy. Ale nie dajcie się zmylić: nie musi to oznaczać, że obserwowane elementy w jakiś sposób przecinają „ramkę przechwytywania” podczas ładowania strony.
Zrzut ekranu DevTools z IntersectionObserver uruchamianym dla wszystkich elementów jednocześnie.
IntersectionObserver zostanie uruchomiony dla wszystkich obserwowanych elementów po ich zarejestrowaniu, ale nie oznacza to, że wszystkie przecinają naszą „klatkę przechwytywania”.

Oznacza to jednak, że wpis dla tego elementu został zainicjowany i jest teraz kontrolowany przez Twój IntersectionObserver . Może to jednak dodać niepotrzebny szum do funkcji zwrotnej, a Twoim obowiązkiem jest wykrycie, które elementy rzeczywiście przecinają „ramkę przechwytywania”, a których nadal nie musimy uwzględniać. Aby zrozumieć, jak przeprowadzić to wykrywanie, zagłębmy się nieco w anatomię naszej funkcji zwrotnej i przyjrzyjmy się, z czego składają się takie wpisy.

Wywołanie zwrotne obserwatora skrzyżowania

Po pierwsze, funkcja wywołania zwrotnego dla obiektu IntersectionObserver przyjmuje dwa argumenty, o których będziemy mówić w odwrotnej kolejności, zaczynając od drugiego argumentu. Wraz ze wspomnianą wcześniej Array obserwowanych wpisów, przecinającą naszą „przechwytującą ramkę”, funkcja wywołania zwrotnego otrzymuje jako drugi argument sam Observer .

Odniesienie do samego obserwatora

 new IntersectionObserver(function(entries, SELF) {…});

Uzyskanie odniesienia do samego Observera jest przydatne w wielu scenariuszach, gdy chcesz przestać obserwować jakiś element po wykryciu go przez IntersectionObserver po raz pierwszy. Tego rodzaju są scenariusze, takie jak leniwe ładowanie obrazów, odroczone pobieranie innych zasobów itp. Gdy chcesz przestać obserwować element, IntersectionObserver udostępnia metodę unobserve(element-to-stop-observing) , którą można uruchomić w funkcji zwrotnej po wykonaniu pewnych działań na obserwowanym elemencie (takich jak rzeczywiste leniwe ładowanie obrazu, na przykład ).

Niektóre z tych scenariuszy zostaną omówione w dalszej części artykułu, ale pomijając ten drugi argument, przejdźmy do głównych aktorów tego odtwarzania wywołań zwrotnych.

SkrzyżowanieObserwatorWejście

 new IntersectionObserver(function(ENTRIES, self) {…});

entries , które otrzymujemy w naszej funkcji zwrotnej jako Array , są specjalnego typu: IntersectionObserverEntry . Interfejs ten zapewnia nam predefiniowany i wstępnie obliczony zestaw właściwości dotyczących każdego konkretnego obserwowanego elementu. Przyjrzyjmy się tym najciekawszym.

Po pierwsze, wpisy typu IntersectionObserverEntry zawierają informacje o trzech różnych prostokątach — definiujących współrzędne i granice elementów biorących udział w procesie:

  • rootBounds : prostokąt dla „ramki przechwytywania” ( root + rootMargin );
  • boundingClientRect : prostokąt dla samego obserwowanego elementu;
  • intersectionRect : Obszar „ramki przechwytywania” przecinany przez obserwowany element.
Prostokąty punktu przecięciaObserverEntry
Wszystkie prostokąty ograniczające zaangażowane w IntersectionObserverEntry są obliczane za Ciebie.

Naprawdę fajną rzeczą w tych prostokątach obliczanych dla nas asynchronicznie jest to, że dostarczają nam ważnych informacji związanych z pozycjonowaniem elementu bez wywoływania przez nas getBoundingClientRect() , offsetTop , offsetLeft i innych kosztownych właściwości i metod pozycjonowania, które powodują zaśmiecanie układu. Czysta wygrana za wydajność!

Inną interesującą nas właściwością interfejsu IntersectionObserverEntry jest isIntersecting . Jest to wygodna właściwość wskazująca, czy obserwowany element aktualnie przecina „klatkę przechwytywania”, czy nie. Moglibyśmy oczywiście uzyskać te informacje, patrząc na intersectionRect (jeśli ten prostokąt nie ma wartości 0×0, element przecina „klatkę przechwytywania”), ale wcześniejsze obliczenie jest dla nas całkiem wygodne.

isIntersecting może służyć do sprawdzenia, czy obserwowany element dopiero wchodzi w „klatkę przechwytywania”, czy już ją opuszcza. Aby się tego dowiedzieć, zapisz wartość tej właściwości jako flagę globalną, a kiedy nowy wpis dla tego elementu dotrze do funkcji wywołania zwrotnego, porównaj nowe isIntersecting z tą flagą globalną:

  • Jeśli był false , a teraz jest true , to element wchodzi w „klatkę przechwytywania”;
  • Jeśli jest odwrotnie i teraz jest false , podczas gdy wcześniej było true , to element opuszcza „klatkę przechwytywania”.

isIntersecting to dokładnie właściwość, która pomaga nam rozwiązać problem, o którym mówiliśmy wcześniej, tj. oddzielne wpisy dla elementów, które naprawdę przecinają „klatkę przechwytywania” z szumem elementów, które są tylko inicjalizacją wpisu.

 let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);

UWAGA : W Microsoft Edge 15 właściwość isIntersecting nie została zaimplementowana, zwracając undefined pomimo pełnego wsparcia dla IntersectionObserver w przeciwnym razie. Zostało to jednak naprawione w lipcu 2017 i jest dostępne od Edge 16.

Interfejs IntersectionObserverEntry udostępnia jeszcze jedną wstępnie obliczoną właściwość wygody: intersectionRatio . Ten parametr może być używany do tych samych celów, co isIntersecting , ale zapewnia bardziej szczegółową kontrolę, ponieważ jest liczbą zmiennoprzecinkową zamiast wartości logicznej. Wartość intersectionRatio wskazuje, jaka część obszaru obserwowanego elementu przecina „ramkę przechwytywania” (stosunek obszaru intersectionRect do obszaru boundingClientRect ). Znowu moglibyśmy wykonać te obliczenia sami, korzystając z informacji z tych prostokątów, ale dobrze jest zrobić to za nas.

Czy to już nie wygląda znajomo? Tak, właściwość <code>intersectionRatio</code> jest podobna do właściwości <code>threshold</code> w konfiguracji Observera. Różnica polega na tym, że ta ostatnia definiuje <em>kiedy</em> odpalić Observera, a pierwsza wskazuje rzeczywistą sytuację na skrzyżowaniu (to jest nieco inne niż <code>próg</code> ze względu na asynchroniczną naturę Observera).
Czy to już nie wygląda znajomo? Tak, właściwość intersectionRatio jest podobna do właściwości threshold w konfiguracji Observera. Różnica polega na tym, że ten drugi definiuje *kiedy* odpalenie Observera, a ten pierwszy wskazuje rzeczywistą sytuację na skrzyżowaniu (czyli nieco inną od threshold ze względu na asynchroniczny charakter Observera).

target to jeszcze jedna właściwość interfejsu IntersectionObserverEntry , do której możesz potrzebować dość często. Ale nie ma tu absolutnie żadnej magii – to tylko oryginalny element, który został przekazany do funkcji observe() Twojego Observera. Podobnie jak event.target , do którego przywykłeś podczas pracy z wydarzeniami.

Aby uzyskać pełną listę właściwości interfejsu IntersectionObserverEntry , sprawdź specyfikację.

Możliwe zastosowania

Zdaję sobie sprawę, że najprawdopodobniej trafiłeś do tego artykułu właśnie z powodu tego rozdziału: kogo obchodzi mechanika, skoro w końcu mamy fragmenty kodu do kopiowania i wklejania? Więc nie będę cię teraz zawracał głowy dalszą dyskusją: wkraczamy w krainę kodu i przykładów. Mam nadzieję, że komentarze zawarte w kodzie wyjaśnią sprawę.

Odroczona funkcjonalność

Przede wszystkim przejrzyjmy przykład ujawniający podstawowe zasady leżące u podstaw idei IntersectionObserver . Powiedzmy, że masz element, który musi wykonać wiele obliczeń, gdy pojawi się na ekranie. Na przykład reklama powinna rejestrować wyświetlenie tylko wtedy, gdy faktycznie została pokazana użytkownikowi. Ale teraz wyobraźmy sobie, że gdzieś poniżej pierwszego ekranu na Twojej stronie znajduje się automatycznie odtwarzany element karuzeli.

Karuzela pod pierwszym ekranem Twojej aplikacji
Kiedy mamy karuzelę lub inną funkcję do podnoszenia ciężkich przedmiotów pod zakładką naszej aplikacji, marnowaniem zasobów jest natychmiastowe rozpoczęcie ładowania/ładowania.

Generalnie prowadzenie karuzeli jest ciężkim zadaniem. Zwykle wiąże się to z licznikami czasu JavaScript, obliczeniami do automatycznego przewijania elementów itp. Wszystkie te zadania ładują główny wątek, a kiedy odbywa się to w trybie autoodtwarzania, trudno nam określić, kiedy nasz wątek główny otrzyma to uderzenie. Kiedy mówimy o nadaniu priorytetu treści na naszym pierwszym ekranie i chcemy jak najszybciej uderzyć w Pierwsze znaczące malowanie i Czas na interaktywność, zablokowany wątek główny staje się wąskim gardłem dla naszej wydajności.

Aby rozwiązać ten problem, możemy odroczyć odtwarzanie takiej karuzeli do czasu, gdy znajdzie się ona w widocznym obszarze przeglądarki. W tym przypadku wykorzystamy naszą wiedzę i przykład dla parametru isIntersecting interfejsu IntersectionObserverEntry .

 const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);

Tutaj odtwarzamy karuzelę tylko wtedy, gdy dostanie się ona do naszego okienka ekranu. Zwróć uwagę na brak obiektu config przekazanego do inicjalizacji IntersectionObserver : oznacza to, że polegamy na domyślnych opcjach konfiguracyjnych. Kiedy karuzela wyjdzie z naszego okienka ekranu, powinniśmy przestać ją odtwarzać, aby nie wydawać zasobów na mniej ważne elementy.

Leniwe ładowanie aktywów

Jest to prawdopodobnie najbardziej oczywisty przypadek użycia IntersectionObserver : nie chcemy wydawać zasobów na pobranie czegoś, czego użytkownik w tej chwili nie potrzebuje. Daje to ogromne korzyści Twoim użytkownikom: użytkownicy nie będą musieli pobierać, a ich urządzenia mobilne nie będą musiały analizować i kompilować wielu bezużytecznych informacji, których w danej chwili nie potrzebują. Nic dziwnego, że poprawi to również wydajność Twojej aplikacji.

Leniwe ładowanie obrazów pod zakładką
Zasoby ładujące się z opóźnieniem, takie jak obrazy znajdujące się pod pierwszym ekranem – najbardziej oczywiste zastosowanie IntersectionObserver.

Wcześniej, aby odroczyć pobieranie i przetwarzanie zasobów do momentu, w którym użytkownik może je wyświetlić na ekranie, mieliśmy do czynienia z detektorami zdarzeń na zdarzeniach takich jak scroll . Problem jest oczywisty: zbyt często wyzwalało to słuchaczy. Musieliśmy więc wpaść na pomysł ograniczenia lub odrzucenia wykonania wywołania zwrotnego. Ale wszystko to spowodowało duży nacisk na nasz główny wątek, blokując go, gdy najbardziej tego potrzebowaliśmy.

Wracając więc do IntersectionObserver w scenariuszu z leniwym ładowaniem, na co powinniśmy zwracać uwagę? Sprawdźmy prosty przykład leniwego ładowania obrazów.

Zobacz ładowanie Pen Lazy w IntersectionObserver autorstwa Denys Mishunov (@mishunov) na CodePen.

Zobacz ładowanie Pen Lazy w IntersectionObserver autorstwa Denys Mishunov (@mishunov) na CodePen.

Spróbuj powoli przewinąć tę stronę do „trzeciego ekranu” i obserwuj okno monitorowania w prawym górnym rogu: poinformuje Cię, ile obrazów zostało do tej pory pobranych.

W rdzeniu znaczników HTML do tego zadania znajduje się prosta sekwencja obrazów:

 … <img data-src="https://blah-blah.com/foo.jpg"> …

Jak widać, obrazy powinny być dostarczane bez tagów src : gdy przeglądarka zobaczy atrybut src , od razu zacznie pobierać ten obraz, co jest sprzeczne z naszymi intencjami. Dlatego nie powinniśmy umieszczać tego atrybutu w naszych obrazach w HTML, a zamiast tego możemy polegać na jakimś atrybucie data- , takim jak data-src tutaj.

Kolejną częścią tego rozwiązania jest oczywiście JavaScript. Skupmy się tutaj na głównych fragmentach:

 const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });

Jeśli chodzi o strukturę, nie ma tu nic nowego: omówiliśmy to wszystko już wcześniej:

  • Otrzymujemy wszystkie wiadomości z naszymi atrybutami data-src ;
  • Ustaw config : w tym scenariuszu chcesz rozszerzyć swoją „klatkę przechwytywania”, aby wykrywać elementy nieco poniżej dolnej części widocznego obszaru;
  • Zarejestruj IntersectionObserver z tą konfiguracją;
  • Iteruj po naszych obrazach i dodaj je wszystkie, aby były obserwowane przez ten IntersectionObserver ;

Ciekawa część ma miejsce w ramach funkcji zwrotnej wywołanej na wpisach. W grę wchodzą trzy podstawowe kroki.

  1. Przede wszystkim przetwarzamy tylko te elementy, które naprawdę przecinają naszą „klatkę przechwytywania”. Ten fragment powinien być Ci już znany.

     entries.forEach(entry => { if (entry.isIntersecting) { … } });

  2. Następnie w jakiś sposób przetwarzamy wpis, konwertując nasz obraz z data-src na rzeczywisty <img src="…"> .

     if (entry.isIntersecting) { preloadImage(entry.target); … }
    Spowoduje to, że przeglądarka w końcu pobierze obraz. preloadImage() to bardzo prosta funkcja, o której nie warto tutaj wspominać. Po prostu przeczytaj źródło.

  3. Kolejny i ostatni krok: ponieważ leniwe ładowanie jest czynnością jednorazową i nie musimy pobierać obrazu za każdym razem, gdy element trafia do naszej „klatki przechwytywania”, powinniśmy unobserve już przetworzonego obrazu. W ten sam sposób, w jaki powinniśmy to zrobić za pomocą element.removeEventListener() dla naszych zwykłych zdarzeń, gdy nie są one już potrzebne, aby zapobiec wyciekom pamięci w naszym kodzie.

     if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as self to our callback self.unobserve(entry.target); }

Notatka. Zamiast unobserve(event.target) moglibyśmy również wywołać Disconnect disconnect() : całkowicie odłącza on nasz IntersectionObserver i nie będzie już obserwował obrazów. Jest to przydatne, jeśli zależy ci tylko na pierwszym trafieniu twojego obserwatora. W naszym przypadku potrzebujemy obserwatora, aby monitorował obrazy, więc nie powinniśmy się jeszcze rozłączać.

Zachęcamy do rozwidlenia przykładu i zabawy z różnymi ustawieniami i opcjami. Jest jednak jedna interesująca rzecz, o której należy wspomnieć , gdy chcesz w szczególności leniwie ładować obrazy. Należy zawsze mieć na uwadze pudełko wygenerowane przez obserwowany element! Jeśli sprawdzisz przykład, zauważysz, że CSS dla obrazów w wierszach 41–47 zawiera rzekomo zbędne style, w tym. min-height: 100px . Ma to na celu nadanie symbolom zastępczym obrazu ( <img> bez atrybutu src ) pewnego wymiaru pionowego. Po co?

  • Bez wymiarów pionowych wszystkie <img> generowałyby pole 0×0;
  • Ponieważ tag <img> domyślnie generuje jakiś rodzaj pola inline-block w wierszu, wszystkie te pola 0×0 zostaną wyrównane obok siebie w tej samej linii;
  • Oznacza to, że Twój IntersectionObserver zarejestrowałby wszystkie (lub, w zależności od szybkości przewijania, prawie wszystkie) obrazy naraz — prawdopodobnie nie do końca to, co chcesz osiągnąć.

Wyróżnienie bieżącej sekcji

Oczywiście IntersectionObserver to znacznie więcej niż tylko leniwe ładowanie. Oto kolejny przykład zastąpienia zdarzenia scroll tą technologią. W tym przypadku mamy dość powszechny scenariusz: na stałym pasku nawigacyjnym powinniśmy podświetlić bieżącą sekcję na podstawie pozycji przewijania dokumentu.

Zobacz sekcję Podświetlanie pisaków w IntersectionObserver autorstwa Denys Mishunov (@mishunov) w CodePen.

Zobacz sekcję Podświetlanie pisaków w IntersectionObserver autorstwa Denys Mishunov (@mishunov) w CodePen.

Strukturalnie jest podobny do przykładu z leniwym ładowaniem obrazów i ma taką samą strukturę podstawową z następującymi wyjątkami:

  • Teraz chcemy obserwować nie obrazy, ale sekcje na stronie;
  • Oczywiście mamy też inną funkcję do przetwarzania wpisów w naszym wywołaniu zwrotnym ( intersectionHandler(entry) ). Ale ten nie jest interesujący: wszystko, co robi, to przełączanie klasy CSS.

Interesujący jest tu jednak obiekt config :

 const config = { rootMargin: '-50px 0px -55% 0px' };

Dlaczego nie pytasz o domyślną wartość 0px dla rootMargin ? Po prostu dlatego, że podświetlenie bieżącej sekcji i leniwe ładowanie obrazu to zupełnie inne cele, które staramy się osiągnąć. Przy leniwym ładowaniu chcemy rozpocząć ładowanie, zanim obraz pojawi się w widoku. Dlatego w tym celu rozszerzyliśmy naszą „klatkę przechwytywania” o 50 pikseli na dole. Wręcz przeciwnie, gdy chcemy podświetlić aktualną sekcję, musimy być pewni, że sekcja jest rzeczywiście widoczna na ekranie. I nie tylko to: musimy być pewni, że użytkownik faktycznie czyta lub zamierza przeczytać dokładnie ten rozdział. Dlatego chcemy, aby sekcja przechodziła nieco ponad połowę rzutni od dołu, zanim będziemy mogli zadeklarować ją jako sekcję aktywną. Ponadto chcemy uwzględnić wysokość paska nawigacyjnego, dlatego usuwamy wysokość paska z „ramki przechwytywania”.

Przechwytywanie ramki dla bieżącej sekcji
Chcemy, aby Observer wykrywał tylko te elementy, które dostają się do „klatki przechwytywania” między 50px od góry a 55% widocznego obszaru od dołu.

Zwróć też uwagę , że w przypadku podświetlenia bieżącego elementu nawigacyjnego nie chcemy przestać niczego obserwować. Tutaj zawsze powinniśmy mieć kontrolę IntersectionObserver , dlatego nie znajdziesz tutaj ani disconnect() , ani unobserve() .

Streszczenie

IntersectionObserver to bardzo prosta technologia. Ma całkiem dobre wsparcie w nowoczesnych przeglądarkach i jeśli chcesz go zaimplementować w przeglądarkach, które nadal (lub wcale) go obsługują, oczywiście jest do tego wypełnienie. Ale ogólnie rzecz biorąc, jest to świetna technologia, która pozwala nam robić różne rzeczy związane z wykrywaniem elementów w widoku, jednocześnie pomagając osiągnąć naprawdę dobry wzrost wydajności.

Dlaczego IntersectionObserver jest dla Ciebie dobry?

  • IntersectionObserver to asynchroniczny nieblokujący interfejs API!
  • IntersectionObserver zastępuje nasze drogie detektory przy zdarzeniach scroll lub resize .
  • IntersectionObserver wykonuje za Ciebie wszystkie kosztowne obliczenia, takie jak getClientBoundingRect() , abyś nie musiał tego robić.
  • IntersectionObserver podąża za strukturalnym wzorcem innych obserwatorów, więc teoretycznie powinno być łatwe do zrozumienia, jeśli znasz sposób działania innych obserwatorów.

Rzeczy, o których należy pamiętać

Jeśli porównamy możliwości IntersectionObserver ze światem window.addEventListener('scroll') , skąd to wszystko się wzięło, trudno będzie dostrzec jakiekolwiek wady tego obserwatora. Zwróćmy więc uwagę na kilka rzeczy, o których należy pamiętać:

  • Tak, IntersectionObserver to asynchroniczny nieblokujący interfejs API. Dobrze wiedzieć! Ale jeszcze ważniejsze jest zrozumienie, że kod, który uruchamiasz w wywołaniach zwrotnych, nie będzie domyślnie uruchamiany asynchronicznie, nawet jeśli sam interfejs API jest asynchroniczny. Więc nadal istnieje szansa na wyeliminowanie wszystkich korzyści, jakie daje IntersectionObserver , jeśli obliczenia funkcji zwrotnej spowodują, że główny wątek przestanie odpowiadać. Ale to już inna historia.
  • Jeśli używasz IntersectionObserver do leniwego ładowania zasobów (takich jak na przykład obrazy), uruchom .unobserve(asset) po załadowaniu zasobu.
  • IntersectionObserver może wykryć przecięcia tylko dla elementów, które pojawiają się w strukturze formatowania dokumentu. Żeby było jasne: obserwowalne elementy powinny generować pudełko i w jakiś sposób wpływać na układ. Oto tylko kilka przykładów, które pomogą Ci lepiej zrozumieć:

    • Elementy z display: none są wykluczone;
    • opacity: 0 lub visibility:hidden tworzą pole (nawet jeśli niewidoczne), aby zostały wykryte;
    • Elementy pozycjonowane bezwzględnie o width:0px; height:0px width:0px; height:0px są w porządku. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negative top , left , etc.) and are cut out by parent's overflow: hidden won't be detected: their box is out of scope for the formatting structure.
IntersectionObserver: Now You See Me
IntersectionObserver: Now You See Me

I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:

  • Intersection Observer API on MDN;
  • IntersectionObserver polyfill;
  • IntersectionObserver polyfill as npm module;
  • Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
  • Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.

With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.