Pensando alla maggior parte dei siti web moderni, soprattutto quelli basati su
come
, è 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
), 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,
. In breve, la soluzione consisterà nel caricare i video specificati all'interno degli elementi <source>
sfruttando
e qualche accorgimento non banale. A ciò seguirà un sapiente uso delle
, in combinazione con l'evento canplaythrough
.
Il codice HTML
Cominciamo subito dando uno sguardo al codice
:
<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à
[!] Ci sono problemi con l'autore. Controllare il mapping sull'Author Manager
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
<video>
setTimeout
race
.then()
can play
video.play()
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.