Acum mă vezi: cum să amân, să încarci leneș și să acționezi cu IntersectionObserver

Publicat: 2022-03-10
Rezumat rapid ↬ Informațiile despre intersecție sunt necesare din mai multe motive, cum ar fi încărcarea leneră a imaginilor. Dar sunt mult mai multe. Este timpul să obțineți o mai bună înțelegere și perspective diferite asupra API-ului Intersection Observer. Gata?

Pe vremuri, a trăit un dezvoltator web care și-a convins cu succes clienții că site-urile nu ar trebui să arate la fel în toate browserele, s-a preocupat de accesibilitate și a fost unul dintre cei mai devreme care a adoptat grilele CSS. Dar în adâncul inimii sale, performanța a fost adevărata lui pasiune: a optimizat, a minimizat, a monitorizat și chiar a folosit trucuri psihologice în proiectele sale.

Apoi, într-o zi, a aflat despre încărcarea leneșă a imaginilor și a altor active care nu sunt imediat vizibile pentru utilizatori și nu sunt esențiale pentru redarea conținutului semnificativ pe ecran. Era începutul zorilor: dezvoltatorul a intrat în lumea diabolică a pluginurilor jQuery cu încărcare leneșă (sau poate în lumea nu atât de diabolică a atributelor async și defer ). Unii spun chiar că a intrat direct în miezul tuturor relelor: lumea ascultătorilor de evenimente de scroll . Nu vom ști niciodată sigur unde a ajuns, dar, din nou, acest dezvoltator este absolut fictiv și orice asemănare cu orice dezvoltator este pur și simplu coincidență.

un dezvoltator web
Dezvoltatorul web fictiv

Ei bine, acum puteți spune că cutia Pandorei a fost deschisă și că dezvoltatorul nostru fictiv nu face problema mai puțin reală. În zilele noastre, prioritizarea conținutului de deasupra paginii a devenit extrem de importantă pentru performanța proiectelor noastre web atât din punct de vedere al vitezei, cât și al greutății paginii.

Mai multe după săritură! Continuați să citiți mai jos ↓

În acest articol, vom ieși din întunericul scroll și vom vorbi despre modul modern de încărcare leneșă a resurselor. Nu doar încărcarea leneșă a imaginilor, ci și încărcarea oricărui activ. Mai mult decât atât, tehnica despre care vom vorbi astăzi este capabilă de mult mai mult decât doar lazy-loading assets: vom putea oferi orice tip de funcționalitate amânată pe baza vizibilității elementelor pentru utilizatori.

IntersectionObserver: Acum mă vezi

Doamnelor și domnilor, să vorbim despre API-ul Intersection Observer. Dar înainte de a începe, să aruncăm o privire la peisajul instrumentelor moderne care ne-a condus la IntersectionObserver .

2017 a fost un an foarte bun pentru instrumentele integrate în browserele noastre, ajutându-ne să îmbunătățim calitatea, precum și stilul bazei noastre de cod fără prea mult efort. În zilele noastre, web-ul pare să se îndepărteze de la soluții sporadice bazate pe soluții foarte diferite de soluții foarte tipice către o abordare mai bine definită a interfețelor Observer (sau doar „Observatori”): MutationObserver bine sprijinit a primit noi membri ai familiei care au fost rapid adoptate în browserele moderne:

  • IntersectionObserver și
  • PerformanceObserver (ca parte a specificației Performance Timeline Level 2).

Încă un potențial membru al familiei, FetchObserver, este o lucrare în desfășurare și ne ghidează mai mult în ținuturile unui proxy de rețea, dar astăzi aș dori să vorbesc mai multe despre front-end.

IntersectionObserver și PerformanceObserver sunt noii membri ai familiei Observers.
IntersectionObserver și PerformanceObserver sunt noii membri ai familiei Observers.

PerformanceObserver și IntersectionObserver urmăresc să ajute dezvoltatorii front-end să îmbunătățească performanța proiectelor lor în diferite puncte. Primul ne oferă instrumentul pentru Monitorizarea utilizatorului real, în timp ce cel de-al doilea este instrumentul, oferindu-ne o îmbunătățire tangibilă a performanței. După cum am menționat anterior, acest articol va arunca o privire detaliată exact asupra celui din urmă: IntersectionObserver . Pentru a înțelege în special mecanica IntersectionObserver , ar trebui să ne uităm la modul în care ar trebui să funcționeze un Observer generic în web-ul modern.

Sfat profesionist : puteți sări peste teoria și să vă scufundați în mecanica IntersectionObserver imediat sau, chiar mai departe, direct la posibilele aplicații ale IntersectionObserver .

Observator vs. Eveniment

Un „Observator”, după cum sugerează și numele, este destinat să observe ceva care se întâmplă în contextul unei pagini. Observatorii pot urmări ceva ce se întâmplă pe o pagină, cum ar fi modificările DOM. Ei pot urmări, de asemenea, evenimentele ciclului de viață ale paginii. Observatorii pot rula și unele funcții de apel invers. Acum, cititorul atent ar putea identifica imediat problema aici și ar putea întreba: „Deci, ce rost are? Nu avem deja evenimente în acest scop? Ce îi face pe Observatori diferiți?” Foarte bun punct! Să aruncăm o privire mai atentă și să rezolvăm.

Observator vs. Eveniment: care este diferența?
Observator vs. Eveniment: Care este diferența?

Diferența crucială dintre Eveniment obișnuit și Observer este că, implicit, primul reacționează sincron pentru fiecare apariție a Evenimentului, afectând capacitatea de răspuns a firului principal, în timp ce cel din urmă ar trebui să reacționeze asincron fără a afecta atât de mult performanța. Cel puțin, acest lucru este valabil pentru observatorii prezentați în prezent: toți se comportă asincron și nu cred că acest lucru se va schimba în viitor.

Acest lucru duce la diferența principală în gestionarea apelurilor inverse ale observatorilor, care ar putea deruta începătorii: natura asincronă a observatorilor poate duce la trecerea mai multor observabile către o funcție de apel invers în același timp. Din acest motiv, funcția de apel invers ar trebui să se aștepte nu la o singură intrare, ci la un Array de intrări (chiar dacă uneori Array va conține o singură intrare în ea).

Mai mult decât atât, unii observatori (în special cel despre care vorbim astăzi) oferă proprietăți pre-calculate foarte utile, pe care, altfel, obișnuiam să ne calculăm folosind metode și proprietăți scumpe (din punct de vedere al performanței) atunci când folosim evenimente obișnuite. Pentru a clarifica acest punct, vom ajunge la un exemplu puțin mai târziu în articol.

Deci, dacă este greu pentru cineva să se îndepărteze de paradigma evenimentului, aș spune că Observatorii sunt evenimente pe steroizi. O altă descriere ar fi: Observatorii reprezintă un nou nivel de aproximare pe deasupra evenimentelor. Dar indiferent de definiția pe care o preferați, ar trebui să fie de la sine înțeles că Observatorii nu sunt menționați să înlocuiască evenimente (cel puțin nu încă); există suficiente cazuri de utilizare pentru ambele și pot trăi fericiți unul lângă altul.

Observatorii nu sunt menționați să înlocuiască Evenimente: ambii pot trăi împreună fericiți.
Observatorii nu sunt menționați să înlocuiască Evenimente: ambii pot trăi împreună fericiți.

Structura generală a observatorului

Structura generică a unui Observator (oricare dintre cele disponibile la momentul scrierii) arată similar cu aceasta:

 /** * 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);

Din nou, rețineți că entries este o Array de valori, nu o singură intrare.

Aceasta este structura generică: Implementările anumitor observatori diferă prin argumentele care sunt transmise în observe() și argumentele trecute în apelul său invers. De exemplu, MutationObserver ar trebui să obțină, de asemenea, un obiect de configurare pentru a ști mai multe despre ce modificări în DOM trebuie observate. PerformanceObserver nu observă noduri în DOM, ci are setul dedicat de tipuri de intrare pe care le poate observa.

Aici, să încheiem partea „generică” a acestei discuții și să ne aprofundăm subiectul articolului de astăzi — IntersectionObserver .

Deconstruirea IntersectionObserver

Deconstruirea IntersectionObserver
Deconstruirea IntersectionObserver

Mai întâi de toate, haideți să înțelegem ce este IntersectionObserver .

Potrivit MDN:

Intersection Observer API oferă o modalitate de a observa în mod asincron schimbările în intersecția unui element țintă cu un element strămoș sau cu fereastra de vizualizare a unui document de nivel superior.

Mai simplu spus, IntersectionObserver observă în mod asincron suprapunerea unui element cu un alt element. Să vorbim despre ce sunt acele elemente în IntersectionObserver .

Inițializare IntersectionObserver

Într-unul din paragrafele anterioare, am văzut structura unui observator generic. IntersectionObserver extinde puțin această structură. În primul rând, acest tip de Observator necesită o configurație cu trei elemente principale:

  • root : Acesta este elementul rădăcină utilizat pentru observație. Acesta definește „cadru de captare” de bază pentru elementele observabile. În mod implicit, root este portul de vizualizare al browserului dvs., dar poate fi într-adevăr orice element din DOM (apoi setați root la ceva de genul document.getElementById('your-element') ). Rețineți totuși că elementele pe care doriți să le observați trebuie să „trăiască” în arborele DOM al root în acest caz.
proprietatea root a configurației lui IntersectionObserver
Proprietatea root definește baza pentru „cadru de captare” pentru elementele noastre.
  • rootMargin : definește marginea din jurul elementului root care extinde sau micșorează „cadru de captare” atunci când dimensiunile root nu oferă suficientă flexibilitate. Opțiunile pentru valorile acestei configurații sunt similare cu cele ale margin în CSS, cum ar fi rootMargin: '50px 20px 10px 40px' (sus, dreapta jos, stânga). Valorile pot fi scurtate (cum ar fi rootMargin: '50px' ) și pot fi exprimate fie în px , fie în % . În mod implicit, rootMargin: '0px' .
proprietatea rootMargin a configurației lui IntersectionObserver
Proprietatea rootMargin extinde/contractează „cadru de captare” care este definit de root .
  • threshold : Nu se dorește întotdeauna să reacționeze instantaneu atunci când un element observat intersectează o margine a „cadrului de captare” (definit ca o combinație de root și rootMargin ). threshold definește procentul de astfel de intersecție la care Observer ar trebui să reacționeze. Poate fi definit ca o singură valoare sau ca o matrice de valori. Pentru a înțelege mai bine efectul threshold (știu că uneori poate fi confuz), iată câteva exemple:
    • threshold: 0 : Valoarea implicită IntersectionObserver ar trebui să reacționeze atunci când primul sau ultimul pixel al unui element observat intersectează una dintre marginile „cadului de captare”. Rețineți că IntersectionObserver este independent de direcție, ceea ce înseamnă că va reacționa în ambele scenarii: a) când elementul intră și b) când iese din „cadru de captare”.
    • threshold: 0.5 : Observatorul ar trebui să fie tras când 50% dintr-un element observat intersectează „cadru de captare”;
    • threshold: [0, 0.2, 0.5, 1] : Observatorul ar trebui să reacționeze în 4 cazuri:
      • Primul pixel al unui element observat intră în „cadru de captare”: elementul nu se află încă în acel cadru, sau ultimul pixel al elementului observat iese din „cadru de captare”: elementul nu se mai află în cadrul;
      • 20% din element se află în „cadru de captare” (din nou, direcția nu contează pentru IntersectionObserver );
      • 50% din element se află în „cadru de captare”;
      • 100% din element se află în „cadru de capturare”. Acesta este strict opus threshold: 0 .
proprietatea prag a configurației lui IntersectionObserver
Proprietatea threshold definește cât de mult ar trebui să intersecteze elementul „cadru de captare” înainte ca Observatorul să fie demis.

Pentru a ne informa IntersectionObserver despre configurația dorită, pur și simplu trecem obiectul nostru de config în constructorul nostru Observer împreună cu funcția noastră de apel invers, astfel:

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

Acum, ar trebui să dăm IntersectionObserver elementul real de observat. Acest lucru se face pur și simplu prin trecerea elementului la funcția observe() :

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

Câteva lucruri de remarcat despre acest element observat:

  • A fost menționat anterior, dar merită menționat din nou: în cazul în care setați root ca element în DOM, elementul observat ar trebui să fie localizat în arborele DOM al root .
  • IntersectionObserver poate accepta doar un element pentru observare la un moment dat și nu acceptă furnizarea de lot pentru observații. Aceasta înseamnă că, dacă trebuie să observați mai multe elemente (să spunem mai multe imagini pe o pagină), trebuie să repetați peste toate și să le observați separat:
 … const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
  • Când încărcați o pagină cu Observer la locul său, este posibil să observați că apelul invers al lui IntersectionObserver a fost declanșat pentru toate elementele observate simultan. Chiar și cele care nu se potrivesc cu configurația furnizată. „Ei bine... nu chiar ceea ce mă așteptam”, este gândul obișnuit când experimentez asta pentru prima dată. Dar nu vă confundați aici: acest lucru nu înseamnă neapărat că acele elemente observate intersectează cumva „cadru de captare” în timp ce pagina se încarcă.
Captură de ecran a DevTools cu IntersectionObserver declanșat pentru toate elementele simultan.
IntersectionObserver va fi declanșat pentru toate elementele observate odată ce acestea sunt înregistrate, dar nu înseamnă că toate se intersectează „cadrul nostru de captare”.

Ceea ce înseamnă totuși, este că intrarea pentru acest element a fost inițializată și este acum controlată de IntersectionObserver . Totuși, acest lucru ar putea adăuga zgomot inutil la funcția de apel invers și devine responsabilitatea dvs. să detectați ce elemente intersectează într-adevăr „cadru de captare” și pentru care încă nu trebuie să luăm în considerare. Pentru a înțelege cum să facem acea detectare, să aprofundăm puțin în anatomia funcției noastre de apel invers și să aruncăm o privire la în ce constau astfel de intrări.

IntersectionObserver Callback

În primul rând, funcția de apel invers pentru un IntersectionObserver ia două argumente și vom vorbi despre acestea în ordine inversă începând cu al doilea argument. Împreună cu Array de intrări observate menționată mai sus, care intersectează „cadrul nostru de captare”, funcția de apel invers primește Observatorul însuși ca al doilea argument.

Referire la observatorul însuși

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

Obținerea referinței la Observatorul în sine este utilă în multe scenarii când doriți să opriți observarea unui element după ce acesta a fost detectat de IntersectionObserver pentru prima dată. Scenariile precum încărcarea leneșă a imaginilor, preluarea amânată a altor active etc. sunt de acest fel. Când doriți să opriți observarea unui element, IntersectionObserver oferă o metodă unobserve(element-to-stop-observing) care poate fi rulată în funcția de apel invers după efectuarea unor acțiuni asupra elementului observat (cum ar fi încărcarea leneșă reală a unei imagini, de exemplu ).

Unele dintre aceste scenarii vor fi analizate în continuare în articol, dar cu acest al doilea argument în afara drumului nostru, să ajungem la actorii principali ai acestei piese de apel invers.

IntersectionObserverEntry

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

entries pe care le primim în funcția noastră de apel invers ca Array sunt de tip special: IntersectionObserverEntry . Această interfață ne oferă un set predefinit și precalculat de proprietăți referitoare la fiecare element observat. Să aruncăm o privire la cele mai interesante.

În primul rând, intrările de tip IntersectionObserverEntry vin cu informații despre trei dreptunghiuri diferite - definind coordonatele și limitele elementelor implicate în proces:

  • rootBounds : Un dreptunghi pentru „cadru de captare” ( root + rootMargin );
  • boundingClientRect : Un dreptunghi pentru elementul observat în sine;
  • intersectionRect : O zonă a „cadru de captare” intersectată de elementul observat.
Dreptunghiuri de IntersectionObserverEntry
Toate dreptunghiurile de delimitare implicate în IntersectionObserverEntry sunt calculate pentru dvs.

Lucrul cu adevărat cool despre aceste dreptunghiuri care sunt calculate pentru noi în mod asincron este că ne oferă informații importante legate de poziționarea elementului, fără să apelăm getBoundingClientRect() , offsetTop , offsetLeft și alte proprietăți și metode scumpe de poziționare care declanșează thrashing layout. Câștig pur pentru performanță!

O altă proprietate a interfeței IntersectionObserverEntry care este interesantă pentru noi este isIntersecting . Aceasta este o proprietate de confort care indică dacă elementul observat intersectează în prezent „cadru de captare” sau nu. Am putea, desigur, să obținem aceste informații uitându-ne la intersectionRect (dacă acest dreptunghi nu este 0×0, elementul intersectează „cadru de captare”), dar este destul de convenabil să avem acest lucru precalculat.

isIntersecting poate fi folosit pentru a afla dacă elementul observat tocmai intră în „cadru de captare” sau îl părăsește deja. Pentru a afla acest lucru, salvați valoarea acestei proprietăți ca indicator global și când noua intrare pentru acest element ajunge la funcția dvs. de apel invers, comparați noul isIntersecting cu acel indicator global:

  • Dacă a fost false și acum este true , atunci elementul intră în „cadru de captare”;
  • Dacă este opusul și este false acum, în timp ce era true înainte, atunci elementul părăsește „cadru de captare”.

isIntersecting este exact proprietatea care ne ajută să rezolvăm problema pe care am discutat mai devreme, adică intrări separate pentru elementele care intersectează cu adevărat „cadru de captare” de zgomotul celor fiind doar inițializarea intrării.

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

NOTĂ : În Microsoft Edge 15, proprietatea isIntersecting nu a fost implementată, revenind undefined , în ciuda suportului complet pentru IntersectionObserver . Acest lucru a fost remediat în iulie 2017 și este disponibil începând cu Edge 16.

Interfața IntersectionObserverEntry oferă încă o proprietate de confort precalculată: intersectionRatio . Acest parametru poate fi folosit în aceleași scopuri ca isIntersecting , dar oferă un control mai granular, deoarece este un număr în virgulă mobilă în loc de o valoare booleană. Valoarea intersectionRatio indică cât de mult din aria elementului observat intersectează „cadru de captare” (raportul dintre zona intersectionRect și zona boundingClientRect ). Din nou, am putea face acest calcul noi înșine folosind informațiile din acele dreptunghiuri, dar este bine să-l facem pentru noi.

Nu pare deja cunoscut? Da, proprietatea <code>intersectionRatio</code> este similară cu proprietatea <code>threshold</code> din configurația lui Observer. Diferența este că cel din urmă definește <em>când</em> să pornească Observer, primul indică situația reală a intersecției (care este ușor diferită de <code>pragul</code> datorită naturii asincrone a Observer).
Nu pare deja cunoscut? Da, proprietatea intersectionRatio este similară cu proprietatea threshold din configurația lui Observer. Diferența este că cel din urmă definește *când* să pornească Observer, primul indică situația reală a intersecției (care este ușor diferită de threshold datorită naturii asincrone a Observer).

target este încă o proprietate a interfeței IntersectionObserverEntry la care ar putea fi necesar să o accesați destul de des. Dar nu există absolut nicio magie aici – este doar elementul original care a fost transmis pentru funcția observe() a observatorului tău. La fel ca event.target cu care te-ai obișnuit atunci când lucrezi cu evenimente.

Pentru a obține lista completă de proprietăți pentru interfața IntersectionObserverEntry , verificați specificația.

Aplicații posibile

Îmi dau seama că cel mai probabil ați ajuns la acest articol tocmai din cauza acestui capitol: cui îi pasă de mecanică când avem fragmente de cod pentru copiere și inserare până la urmă? Deci nu vă voi deranja cu mai multe discuții acum: intrăm în țara codului și a exemplelor. Sper că comentariile incluse în cod vor clarifica lucrurile.

Funcționalitate amânată

În primul rând, să revizuim un exemplu care dezvăluie principiile de bază care stau la baza ideii de IntersectionObserver . Să presupunem că aveți un element care trebuie să facă o mulțime de calcule odată ce este pe ecran. De exemplu, anunțul dvs. ar trebui să înregistreze o vizualizare numai atunci când a fost afișat efectiv unui utilizator. Dar acum, să ne imaginăm că aveți un element carusel redat automat undeva sub primul ecran de pe pagina dvs.

Carusel sub primul ecran al aplicației dvs
Când avem un carusel sau orice altă funcționalitate de ridicare grea sub pliul aplicației noastre, este o risipă de resurse să începem să o încărcați imediat.

Conducerea unui carusel, în general, este o sarcină grea. De obicei, implică temporizatoare JavaScript, calcule pentru derularea automată a elementelor etc. Toate aceste sarcini încarcă firul principal, iar când se face în modul de redare automată, ne este greu să știm când firul nostru principal primește această lovitură. Când vorbim despre prioritizarea conținutului de pe primul nostru ecran și vrem să atingem First Meaningful Paint și Time To Interactive cât mai curând posibil, firul principal blocat devine un blocaj pentru performanța noastră.

Pentru a remedia problema, este posibil să amânăm redarea unui astfel de carusel până când intră în fereastra de vizualizare a browserului. În acest caz, ne vom folosi cunoștințele și exemplul pentru parametrul isIntersecting al interfeței 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);

Aici, jucăm carusel numai când intră în fereastra noastră. Observați absența obiectului de config transmis inițializării lui IntersectionObserver : aceasta înseamnă că ne bazăm pe opțiunile implicite de configurare. Când caruselul iese din fereastra noastră de vizualizare, ar trebui să încetăm să-l mai jucăm pentru a nu cheltui resurse pe elementele care nu mai sunt importante.

Încărcare leneșă a activelor

Acesta este, probabil, cel mai evident caz de utilizare pentru IntersectionObserver : nu dorim să cheltuim resurse pentru a descărca ceva de care utilizatorul nu are nevoie acum. Acest lucru va oferi un beneficiu imens utilizatorilor dvs.: utilizatorii nu vor trebui să descarce, iar dispozitivele lor mobile nu vor trebui să analizeze și să compileze o mulțime de informații inutile de care nu au nevoie în acest moment. Deloc surprinzător, va ajuta și performanța aplicației dvs.

Imagini care se încarcă leneș sub pliul
Încărcare leneră a activelor, cum ar fi imaginile situate sub primul ecran – cea mai evidentă aplicație a IntersectionObserver.

Anterior, pentru a amâna descărcarea și procesarea resurselor până în momentul în care utilizatorul le-ar putea afișa pe ecran, aveam de-a face cu ascultători de evenimente la evenimente precum scroll . Problema este evidentă: acest lucru i-a declanșat pe ascultători mult prea des. Așa că a trebuit să venim cu ideea de a reduce sau anula execuția callback-ului. Dar toate acestea au adăugat multă presiune asupra firului nostru principal, blocându-l atunci când aveam cea mai mare nevoie.

Deci, revenind la IntersectionObserver într-un scenariu de încărcare leneșă, la ce ar trebui să fim atenți? Să verificăm un exemplu simplu de imagini cu încărcare leneșă.

Vedeți încărcarea Pen Lazy în IntersectionObserver de Denys Mishunov (@mishunov) pe CodePen.

Vedeți încărcarea Pen Lazy în IntersectionObserver de Denys Mishunov (@mishunov) pe CodePen.

Încercați să defilați încet acea pagină la „al treilea ecran” și urmăriți fereastra de monitorizare din colțul din dreapta sus: vă va anunța câte imagini au fost descărcate până acum.

În nucleul de markup HTML pentru această sarcină se află o secvență simplă de imagini:

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

După cum puteți vedea, imaginile ar trebui să vină fără etichete src : odată ce un browser vede atributul src , va începe să descarce imediat acea imagine, care este opusă intențiilor noastre. Prin urmare, nu ar trebui să punem acel atribut pe imaginile noastre în HTML și, în schimb, ne-am putea baza pe un atribut de data- precum data-src aici.

O altă parte a acestei soluții este, desigur, JavaScript. Să ne concentrăm aici pe principalele biți:

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

Din punct de vedere al structurii, nu este nimic nou aici: am acoperit toate acestea înainte:

  • Primim toate mesajele cu atributele noastre data-src ;
  • Set config : pentru acest scenariu doriți să extindeți „cadru de captare” pentru a detecta elemente puțin mai jos decât partea de jos a ferestrei de vizualizare;
  • Înregistrați IntersectionObserver cu acea configurație;
  • Repetați imaginile noastre și adăugați-le pe toate pentru a fi observate de acest IntersectionObserver ;

Partea interesantă se întâmplă în cadrul funcției de apel invers invocate pe intrări. Sunt trei pași esențiali implicați.

  1. În primul rând, procesăm doar elementele care se intersectează cu adevărat „cadru de captare”. Acest fragment ar trebui să vă fie familiar până acum.

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

  2. Apoi, procesăm cumva intrarea prin conversia imaginii noastre cu data-src într-un <img src="…"> real.

     if (entry.isIntersecting) { preloadImage(entry.target); … }
    Acest lucru va declanșa browserul să descarce în sfârșit imaginea. preloadImage() este o funcție foarte simplă care nu merită menționată aici. Citiți doar sursa.

  3. Următorul și ultimul pas: deoarece încărcarea leneșă este o acțiune unică și nu trebuie să descărcam imaginea de fiecare dată când elementul intră în „cadru de captare”, ar trebui să ne unobserve imaginea deja procesată. Același mod în care ar trebui să o facem cu element.removeEventListener() pentru evenimentele noastre obișnuite când acestea nu mai sunt necesare pentru a preveni scurgerile de memorie în codul nostru.

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

Notă. În loc de unobserve(event.target) , am putea apela și disconnect() : deconectează complet IntersectionObserver și nu ar mai observa imaginile. Acest lucru este util dacă singurul lucru la care îți pasă este prima lovitură pentru observatorul tău. În cazul nostru, avem nevoie ca Observer să continue să monitorizeze imaginile, așa că nu ar trebui să ne deconectăm încă.

Simțiți-vă liber să treceți la exemplu și să jucați cu diferite setări și opțiuni. Există un lucru interesant de menționat, totuși, atunci când doriți să încărcați leneș imaginile în special. Ar trebui să păstrați întotdeauna cutia, generată de elementul observat! Dacă verificați exemplul, veți observa că CSS-ul pentru imaginile de pe liniile 41–47 conține stiluri presupuse redundante, inclusiv. min-height: 100px . Acest lucru se face pentru a da substituenților imaginii ( <img> fără atributul src ) o dimensiune verticală. Pentru ce?

  • Fără dimensiuni verticale, toate etichetele <img> ar genera caseta 0×0;
  • Deoarece eticheta <img> generează un fel de casetă de inline-block în mod implicit, toate aceste casete 0×0 ar fi aliniate una lângă alta pe aceeași linie;
  • Aceasta înseamnă că IntersectionObserver va înregistra toate (sau, în funcție de cât de repede derulați, aproape toate) imaginile simultan - probabil că nu este exact ceea ce doriți să obțineți.

Evidențierea secțiunii curente

IntersectionObserver este mult mai mult decât încărcare leneșă, desigur. Iată un alt exemplu de înlocuire a evenimentului de scroll cu această tehnologie. În acesta avem un scenariu destul de comun: pe bara de navigare fixă ​​ar trebui să evidențiem secțiunea curentă în funcție de poziția de defilare a documentului.

Consultați secțiunea curentă Evidențierea stiloului din IntersectionObserver de Denys Mishunov (@mishunov) pe CodePen.

Consultați secțiunea curentă Evidențierea stiloului din IntersectionObserver de Denys Mishunov (@mishunov) pe CodePen.

Din punct de vedere structural, este similar cu exemplul pentru imaginile cu încărcare leneră și are aceeași structură de bază, cu următoarele excepții:

  • Acum vrem să observăm nu imagini, ci secțiunile din pagină;
  • În mod evident, avem și o funcție diferită pentru a procesa intrările din callback ( intersectionHandler(entry) ). Dar acesta nu este interesant: tot ce face este să comute între clasa CSS.

Totuși, ceea ce este interesant aici este obiectul de config :

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

De ce nu valoarea implicită de rootMargin 0px vă întrebați? Ei bine, pur și simplu pentru că evidențierea secțiunii curente și încărcarea leneșă a unei imagini sunt destul de diferite în ceea ce încercăm să realizăm. Cu încărcarea leneșă, dorim să începem încărcarea înainte ca imaginea să intre în vizualizare. Prin urmare, în acest scop, ne-am extins „cadru de captare” cu 50 px în partea de jos. Dimpotrivă, atunci când vrem să evidențiem secțiunea curentă, trebuie să ne asigurăm că secțiunea este de fapt vizibilă pe ecran. Și nu numai atât: trebuie să fim siguri că acel utilizator citește sau va citi exact această secțiune. Prin urmare, dorim ca o secțiune să parcurgă puțin mai mult de jumătate din fereastra de vizualizare din partea de jos înainte de a o putea declara secțiunea activă. De asemenea, dorim să luăm în considerare înălțimea barei de navigare și, prin urmare, eliminăm înălțimea barei din „cadru de capturare”.

Se captează cadrul pentru secțiunea curentă
Dorim ca Observer să detecteze doar elementele care intră în „cadru de captare” între 50 px din partea de sus și 55% din fereastra de vizualizare din partea de jos.

De asemenea, rețineți că în cazul evidențierii elementului de navigare curent, nu dorim să încetăm să observăm nimic. Aici ar trebui să păstrăm întotdeauna la conducere IntersectionObserver , prin urmare nu veți găsi nici disconnect() nici unobserve() aici.

rezumat

IntersectionObserver este o tehnologie foarte simplă. Are un suport destul de bun în browserele moderne și dacă doriți să îl implementați pentru browsere care încă (sau nu îl vor suporta deloc), desigur, există un polyfill pentru asta. Dar, per total, aceasta este o tehnologie grozavă care ne permite să facem tot felul de lucruri legate de detectarea elementelor dintr-o fereastră de vizualizare, ajutând în același timp la obținerea unui spor de performanță foarte bun.

De ce este IntersectionObserver bun pentru tine?

  • IntersectionObserver este un API asincron care nu blochează!
  • IntersectionObserver înlocuiește ascultătorii noștri scumpi la evenimentele de scroll sau resize .
  • IntersectionObserver face toate calculele costisitoare precum getClientBoundingRect() pentru tine, astfel încât să nu fie nevoie.
  • IntersectionObserver urmează modelul structural al altor observatori, așa că, teoretic, ar trebui să fie ușor de înțeles dacă ești familiarizat cu modul în care funcționează alți observatori.

Lucruri de reținut

Dacă comparăm capacitățile lui IntersectionObserver cu lumea window.addEventListener('scroll') de unde a venit totul, va fi greu să vedem orice dezavantaje în acest Observer. Așadar, să notăm câteva lucruri de care trebuie să ținem cont:

  • Da, IntersectionObserver este un API asincron care nu blochează. Este grozav de știut! Dar este și mai important să înțelegeți că codul pe care îl rulați în apelurile dvs. inverse nu va fi rulat asincron în mod implicit, chiar dacă API-ul în sine este asincron. Deci, există încă șansa de a elimina toate beneficiile pe care le obțineți de la IntersectionObserver dacă calculele funcției de apel invers fac ca firul principal să nu răspundă. Dar aceasta este o altă poveste.
  • Dacă utilizați IntersectionObserver pentru încărcarea leneră a activelor (cum ar fi imaginile, de exemplu), rulați .unobserve(asset) după ce materialul a fost încărcat.
  • IntersectionObserver poate detecta intersecții doar pentru elementele care apar în structura de formatare a documentului. Pentru a fi clar: elementele observabile ar trebui să genereze o casetă și să afecteze cumva aspectul. Iată doar câteva exemple pentru a vă oferi o mai bună înțelegere:

    • Elemente cu display: none este exclus;
    • opacity: 0 sau visibility:hidden creează caseta (deși invizibilă), astfel încât acestea să fie detectate;
    • Elemente poziționate absolut cu width:0px; height:0px width:0px; height:0px sunt în regulă. 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.