Pensando alla maggior parte dei siti web moderni, soprattutto quelli basati su CMS come WordPress, è facile convincersi che molti di essi utilizzano un layout abbastanza simile, quasi uno standard de facto. C'è un'immagine o uno slider in alto che identifica il brand o ne mostra i principali prodotti, seguito poi da sezioni più o meno variabili. In alcuni casi, però, tale slider è sostituito da un video, che può rendere più accattivante l'aspetto del sito. E non mancano soluzioni non convenzionali (come questa), che sfruttano i video addirittura come sfondo full screen.
Eppure, la scelta di includere video su siti web moderni potrebbe sembrare un po' azzardata, soprattutto se non si provvede opportunamente alla gestione del loro caricamento, e del conseguente impatto sulla banda. Un video, infatti, è in genere molto pesante (soprattutto se la sua qualità è elevata), e non è particolarmente adatto a tutti i contesti, soprattutto nei casi in cui la banda a disposizione del browser è molto limitata.
In questa lezione mostreremo come implementare un meccanismo per il lazy loading di un video. La soluzione che discuteremo sarà basata su quella proposta dallo sviluppatore Ben Robertson, disponibile su GitHub. In breve, la soluzione consisterà nel caricare i video specificati all'interno degli elementi <source>
sfruttando JavaScript e qualche accorgimento non banale. A ciò seguirà un sapiente uso delle promise, in combinazione con l'evento canplaythrough
.
Il codice HTML
Cominciamo subito dando uno sguardo al codice HTML:
<video class="js-video-loader" poster="images/poster.jpg" muted="true" loop="true">
<source data-src="videos/video.webm" type="video/webm">
<source data-src="videos/video.mp4" type="video/mp4">
</video>
La prima cosa da notare è il fatto che tutti i tag <source>
non hanno alcun attributo src
: i path dei due video, infatti, sono assegnati agli attributi data-src
. Questa scelta evita che il browser carichi automaticamente i video, permettendoci di posticiparne il caricamento e renderlo programmatico sfruttando JavaScript. L'unica cosa ad essere effettivamente caricata è l'immagine specificata dall'attributo poster
.
Il codice JavaScript
A questo punto, faremo riferimento alla classe JavaScript menzionata all'inizio di questa lezione. Diamo uno sguardo al costruttore:
constructor () {
this.videos = Array.from(document.querySelectorAll('video.js-video-loader'));
if (typeof Promise === 'undefined'
|| !this.videos
|| window.matchMedia('(prefers-reduced-motion)').matches
|| window.innerWidth < 992
) {
return;
}
this.videos.forEach(this.loadVideo.bind(this));
}
Questo costruttore non fa altro che selezionare tutti i video con classe js-video-loader
, ed eseguire il metodo loadVideo
su ognuno di essi. Tutto ciò a meno che:
- non siano supportate le promise
- nessun video della pagina ha classe
js-video-loader
- nel caso la proprietà CSS
prefers-reduced-motion
sia impostata sureduce
- lo schermo sia piccolo (quest'ultima possibilità, così come la precedente, potrebbe tranquillamente essere omessa; qui è mantenuta, di fatto, per disabilitare il lazy loading per gli schermi più piccoli)
A questo punto, diamo un'occhiata al metodo loadVideo()
:
loadVideo(video) {
this.setSource(video);
video.load();
this.checkLoadTime(video);
}
Di questo metodo, la parte più interessante è la prima riga, quella che esegue setSource
. Vediamone il codice:
setSource (video) {
let children = Array.from(video.children);
children.forEach(child => {
if ( child.tagName === 'SOURCE'
&& typeof child.dataset.src !== 'undefined' ) {
child.setAttribute('src', child.dataset.src);
}
});
}
In pratica, non facciamo altro che assegnare ad ogni tag <source>
di ogni video, un nuovo attributo src
con il path specificato inizialmente su data-src
. A seguito di ciò, l'esecuzione di video.load()
in loadVideo
avvia il caricamento vero e proprio.
La porzione di codice forse più interessante si trova però nella definizione del metodo checkLoadTime()
:
checkLoadTime(video) {
// Creiamo la prima promise, che si risolve
// in corrispondenza dell'evento video.canplaythrough
const videoLoad = new Promise((resolve) => {
video.addEventListener('canplaythrough', () => {
resolve('can play');
});
});
// Creiamo la seconda promise, che si risolve
// dopo un tempo predefinito (2 secondi)
const videoTimeout = new Promise((resolve) => {
setTimeout(() => {
resolve('The video timed out.');
}, 2000);
});
// Sfruttiamo il metodo race
Promise.race([videoLoad, videoTimeout]).then((data) => {
if (data === 'can play') {
video.play();
setTimeout(() => {
video.classList.add('video-loaded');
}, 500);
}
else {
this.cancelLoad(video);
}
});
}
Vengono create due promise. La prima è risolta quando viene generato l'evento canplaythrough
dal tag <video>
, che a sua volta è creato dal browser quando il video è stato caricato parzialmente, ma quanto basta per eseguirlo tutto senza interruzioni fino alla fine. L'altra promise, invece, funge da timer, essendo risolta dopo 2 secondi mediante setTimeout
. Possiamo quindi verificare quale delle due promise termina prima, sfruttando il metodo race
. Nel metodo .then()
, verifichiamo semplicemente se riceviamo la stringa can play
: in caso affermativo, il video potrà partire (video.play()
), altrimenti potremo annullare il caricamento.
Il metodo cancelLoad
(l'ultimo che rimane da analizzare) fa in pratica l'opposto di quanto visto con loadVideo
, rimuovendo i path dei video dagli attributi src
, ed eseguendo nuovamente video.load()
per resettare l'elemento:
cancelLoad (video) {
let children = Array.from(video.children);
children.forEach(child => {
if ( child.tagName === 'SOURCE'
&& typeof child.dataset.src !== 'undefined' ) {
child.parentNode.removeChild(child);
}
});
// Ricarico il video senza source, per resettarlo
video.load();
}
Conclusioni
La soluzione proposta non risolve tutti i problemi, in quanto una connessione limitata impedirebbe comunque il caricamento dell'intero video. Tuttavia, ciò significa anche che probabilmente la disponibilità di banda non è adeguata alla riproduzione di un video di grandi dimensioni, e probabilmente è meglio risparmiare la connessione per caricare altri contenuti.