În zilele de azi când necesitatea unui site responsive și rapid nu mai e un lux, dezvoltatorii încearcă pe toate căile să minimizeze timpii de încărcare în acord cu diferitele rezoluții ale terminalelor pe care site-urile rulează.Una din secțiunile potențial optimizabile este zona de contact, informatii, formular, și da, ați ghicit cunoscuta hartă Google care arată utilizatotului care este localizarea ta geografica, și cum (eventual) se poate ajunge la tine.

O informație folositoare și care susține credibilitatea celui pe care site-ul îl reprezintă. Numai că...

Preluarea 'oarba' a codului de hartă din pagina Google Maps nu e ok, se copiază un iframe static de fapt, cu dimensiuni fixe, și care de cele mai multe ori nu se potriveste nicicum cu layout-ului secțiunii de contact. De asemenea poziționarea aproape de footer-ul siteului face ca nu toți utilizatorii să ajungă până acolo, făcând din încarcărea hărtii o risipă inutilă de resurse și o reducere notabilă a vitezei de încărcare a paginii. (Din Lighthouse și PageSpeed Insights se vede bine de tot acest bottleneck...)

Deci cum procedam ca să optimizam lucrurile. Pe rând.

A. RELATIV LA RESPONSIVITATE
Definim un container (o sectiune division) ca și depozitar al hărții, și cu ceva reguli CSS  se poate rezolva problema responsivității. De regulă ce preluăm cu 'copy&paste' de la https://www.google.ro/maps este un cod care arată cam așa:

<!-- Height=450px; Width=600px -->
<iframe src="https://www.google.com/maps/embed?pb=!1m14!1m12!1m3!1d7098.94326104394!2d78.0430654485247!3d27.172909818538997!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385710909804" width="600" height="450" frameborder="0" style="border:0"></iframe>

Dupa cum se vede, lățimea și lungime implicite sunt  fixe pentru iframe. Dacă dorim să redefinim aceasta hartă statica într-una dinamică, responsiva, tot ce avem de facut e să definim un wrapper, un division container, la care să atașam ceva reguli CSS și să punem codul cu iframe inauntru.
Arată cam așa:

Codul CSS


<style>
      .google-map-container {
          position: relative;
          padding-bottom: 75%; /* aceasta este rata aspectului hartii și se calculează asa (450/600 = .75, aka 75% ).
Dacă doriți harta la dimensiuni diferite pur și simplu refaceți calculul la noile valori */
          height: 0;
          overflow: hidden;
      }
      .google-map-container iframe {
          position: absolute;
          top: 0;
          left: 0;
          width: 100% !important;
          height: 100% !important;
      }
</style>

Codul HTML


<div style='width:50%; margin-auto;'>
<div class="google-map-container">
<iframe src="https://www.google.com/maps/embed?pb=!1m14!1m12!1m3!1d7098.94326104394!2d78.0430654485247!3d27.172909818538997!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385710909804" width="600" height="450" frameborder="0" style="border:0"></iframe>
</div>
</div>


Implementați acest cod și modificați mărimea ferestrei. Se vede cum dimensiunile ferestrei se modifica. Tehnica poate fi folosita și la embedded videos, și la imagini, dar la acestea exista o metodă mult mai simplă.

 

B. RELATIV LA LAZY LOADING
Problema 'incărcarii târzii' e un pic mai complicată. In esență dorim ca încarcărea harții Google să se facă numai atunci când utilizatorul a ajuns la ea, adică făcand scroll, containerul
care găzduiește harta a ajuns să fie văzut de utilizator. In acel moment ar trebui ca browserul să apeleze Google si să aduca informația local.
Sunt doua metode de realizare.


1.Old School way.
Se implementează un EventListener pentru un event de tip scroll. Adică ori de cate ori utilizatorul face scroll, listener-ul declanșează un set de operații:
se verifică (calculează) dacă 'cantitatea de scroll' (scuze, n-am gasit o sintagma mai bună) este egală sau mai mare decat poziția (offsetul) containerului față de top-ul documentului curent.
Dacă da, atunci prima linie de pixeli a containerului hărții a intrat în vizualizarea utilizatorului, prin urmare declanșează încarcărea. Apoi anuleaza EventListenerul pe scroll, nu mai avem nevoie de el.
Dacă nu, continuă și calculează.
Evident metoda presupune o încărcare destul de serioasî a firului de execuție și unele browsere mai vechi, sau mai puțin 'potente' pot afișa un scroll sacadat, nu prea good user experience.


2. Mai aproape de zilele noastre.
Se foloseste IntersectionObserver API o interfață modernă cu care se pot manageria acțiuni pe o pagină de web în funcție de cum și ce elemente de pe aceasta pagină interacționează în diverse moduri.
Definiția dupa MDN:
"Intersection Observer API oferă o modalitate de a observa în mod asincron intersecția unui element target cu un element părinte, sau cu fereastra de vizualizare a documentului respectiv (viewport)."
In esență ce avem aici ?

  • Avem un observant, sau root element, sau element 'rădacină' al observației; implicit viewport, explicit orice element care poate fi afișat în mod block, adică are granițe definite, dreptunghiulare, sau poate fi încadrat in acest pattern,
  • Avem un observat 'child of observant', adica în DOM, observantul este părintele observatului,
  • Avem un obiect care defineste cum se configurează actul observației și cine participă
  • Avem un obiect care este instanța clasei IntersectionObserver.

Iata un exemplu generic cu comentariile de rigoare:

//obiectul care defineste configurarea observației
const config = {
root: null, //observant, elementul folosit pentru observașie; daca este null, implicit va fi definit ca viewportul browserului, altfel poate fi  orice element block din DOM
rootMargin: '0px', //dacă în jurul elementului root vrem să definim o arie 'activă' pentru mai multa flexibilitate; similar cu definitia CSS margin
treshold: 0 //procentajul de 'intersecție' a elementelor care declansează acțiunea, între 0 - la primul pixel din observat, 1 - la ultimul pixel din observat; poate fi o valoare sau un array de ex. [0,.5, 1]
           //actiunea se declansează la fiecare valoare;
}
//mai jos este obiectul instanțiat; parametrul pasat este o funcție care la rândul ei are obligatoriu un vector cu referinte la elementele observate ca parametru, și o auoreferință (SELF)
let observer = new IntersectionObserver(function(entries, SELF){
            //se traverseză vectorul de observați (entries); daca observatul este observat (:-), okay, am notat și fac unobserve pe dansul;
            //de notat ca toata greutatea calcului, inclusiv continuitatea actului de observare cade în sarcina motorului browserului, ceea ce face                procesul foarte puțin obtruziv.
             entries.forEach( function(ivalue,index){
                   if( ivalue.isIntersecting){
                         console.log("EXISTĂ O INTERSECTIE !");
                         SELF.unobserve(ivalue.target);
                   }
            });
}, config);
//observat este referinta din DOM a unui element children al observantului; unde observantul este, așa cum am zis fie viewportul, fie alt element din DOM, parinte pentru observat
observer.observe( observat );

Simplu și direct.

Și iată o implementare completă pentru o harta Google.


<!doctype html>
<html>
<head>
<style>
          .google-map-container {
               position: relative;
               padding-bottom: 56.25%;
               height: 0;
               overflow: hidden;
           }
          .google-map-container iframe {
               position: absolute;
               top: 0;
               left: 0;
               width: 100%;
               height: 100%;
           }
</style>
</head>
<body>
<h3>Scroll down pana la hartă...</h3>
<div style='margin-top:1500px; margin-bottom:100px; width:50%; margin-left:auto; margin-right:auto;'>
<div id='observed' class="google-map-container"></div>
</div>
<script>
const config = {
           root: null,
           rootMargin: '0px',
           treshold: 0
}
const observed = document.getElementById('observed')
const observedContent = "<iframe src='https://www.google.com/maps/embed?pb=!1m16!1m12!1m3!1d42940.62353427634!2d23.919173549402906!3d47.72734475704244!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!2m1!1zSlVELiBNQVJBTVVSRciYLCBTQVQuIEJVREXImFRJLCBDT00uIEJVREXImFRJLCBCVURFyJhUSSwgTlIuOTc!5e0!3m2!1sro!2sro!4v1618473392986!5m2!1sro!2sro' style='border:none;' allowfullscreen=''></iframe> ";
let observer = new IntersectionObserver(function(entries, SELF){
                entries.forEach( function(ivalue,index){
                       if( ivalue.isIntersecting){
                           observed.innerHTML = observedContent;
                           SELF.unobserve(ivalue.target);
                       }
                });
}, config);
observer.observe( observed ); //argumentul poate fi doar DOM Element</script></body></html>

Se poate testa live, AICI.


Suportul pentru browsere este destul de okay.
Această tehnica deschide largi posibiltăți. Tot ce nu este ABOVE THE FOLD poate fi amânat și încarcat ON DEMAND. Imaginați-vă un carusel cu o mulțime de cod javascript scump în resurse și timp de inițializare/execuție.
Care generează valori mari pentru Total Time Blocking sau Speed Index in Lighthouse. Sau o galerie de imagini de zeci/sute de imagini. Toate pot fi încărcate doar la momentul vizualizării.
Sau un video care pornește / se operește cănd întră, respectiv când iese din view.

Cam atât, întrebări, obsevații, critici astept cu drag :-)