Parallasse'

Paul Lewis

Introduzione

Di recente, i siti con effetto parallasse sono stati molto apprezzati. Dai un'occhiata a questi:

Se non li conosci, si tratta di siti in cui la struttura visiva della pagina cambia mentre scorri. In genere, gli elementi all'interno della pagina vengono ridimensionati, ruotati o spostati in proporzione alla posizione di scorrimento della pagina.

Una pagina dimostrativa con effetto parallasse
La nostra pagina demo completa con effetto parallasse

Che ti piacciano o meno i siti con effetto parallasse è un conto, ma quello che puoi dire con certezza è che sono un buco nero per le prestazioni. Il motivo è che i browser tendono a essere ottimizzati per il caso in cui nuovi contenuti vengano visualizzati nella parte superiore o inferiore dello schermo quando scorri (a seconda della direzione di scorrimento) e, in termini generali, i browser funzionano meglio quando le modifiche visive durante uno scorrimento sono minime. Per un sito con effetto parallasse, questo accade raramente, poiché molte volte gli elementi visivi di grandi dimensioni in tutta la pagina cambiano, causando una nuova colorazione dell'intera pagina da parte del browser.

È ragionevole generalizzare un sito con effetto parallasse come questo:

  • Elementi di sfondo che, quando scorri verso l'alto e verso il basso, cambiano posizione, rotazione e scala.
  • Contenuti della pagina, come testo o immagini più piccole, che scorrono nel modo tipico dall'alto verso il basso.

In precedenza abbiamo trattato il rendimento dello scorrimento e i modi in cui puoi cercare di migliorare la reattività della tua app. Questo articolo si basa su queste nozioni, quindi potrebbe valere la pena leggerlo se non l'hai ancora fatto.

La domanda è: se stai creando un sito con scorrimento parallattico, devi necessariamente utilizzare costosi ritocchi o esistono approcci alternativi che puoi adottare per massimizzare il rendimento? Diamo un'occhiata alle opzioni disponibili.

Opzione 1: utilizza gli elementi DOM e le posizioni assolute

Sembra che questo sia l'approccio predefinito adottato dalla maggior parte delle persone. La pagina contiene molti elementi e ogni volta che viene attivato un evento di scorrimento vengono eseguiti una serie di aggiornamenti visivi per trasformarli.

Se avvii la cronologia di DevTools in modalità frame e scorri, noterai che sono presenti operazioni di pittura a schermo intero costose e, se scorri molto, potresti vedere diversi eventi di scorrimento all'interno di un singolo frame, ognuno dei quali attiverà il lavoro di layout.

Strumenti per sviluppatori di Chrome senza eventi di scorrimento con debounce.
DevTools che mostra grandi pitture e più layout attivati da eventi in un unico frame.

L'importante da tenere a mente è che per raggiungere i 60 fps (corrispondenti alla frequenza di aggiornamento tipica del monitor di 60 Hz) abbiamo poco più di 16 ms per completare tutte le operazioni. In questa prima versione, eseguiamo gli aggiornamenti visivi ogni volta che riceviamo un evento di scorrimento, ma come abbiamo discusso negli articoli precedenti sulle animazioni più snelle e compatte con requestAnimationFrame e sul rendimento dello scorrimento, questo non coincide con la pianificazione degli aggiornamenti del browser, quindi perdiamo frame o eseguiamo troppo lavoro in ogni frame. Ciò potrebbe facilmente dare al tuo sito un aspetto innaturale e instabile, con conseguente delusione degli utenti e infelicità dei gattini.

Spostiamo il codice di aggiornamento dall'evento di scorrimento a un callback requestAnimationFrame e acquisiamo semplicemente il valore di scorrimento nel callback dell'evento di scorrimento.

Se ripeti il test di scorrimento, potresti notare un leggero miglioramento, anche se non molto. Il motivo è che l'operazione di layout attivata tramite lo scorrimento non è molto dispendiosa, ma in altri casi d'uso potrebbe esserlo. Ora eseguiamo almeno un'operazione di layout in ogni frame.

Chrome DevTools con eventi di scorrimento debounced.
DevTools che mostra grandi pitture e più layout attivati da eventi in un unico frame.

Ora possiamo gestire uno o cento eventi di scorrimento per frame, ma, soprattutto, memorizziamo solo il valore più recente da utilizzare ogni volta che viene eseguito il callback requestAnimationFrame ed eseguiamo gli aggiornamenti visivi. Il punto è che hai smesso di tentare di forzare gli aggiornamenti visivi ogni volta che ricevi un evento di scorrimento e hai chiesto al browser di fornirti una finestra appropriata in cui eseguirli. Non sei gentile?

Il problema principale di questo approccio, requestAnimationFrame o meno, è che abbiamo essenzialmente un livello per l'intera pagina e, spostando questi elementi visivi, sono necessari ricostruzioni di grandi dimensioni (e costose). In genere, la pittura è un'operazione di blocco (anche se sta cambiando), il che significa che il browser non può fare altro lavoro e spesso superiamo di molto il budget del frame di 16 ms e le cose rimangono instabili.

Opzione 2: utilizza elementi DOM e trasformazioni 3D

Invece di utilizzare posizioni assolute, un altro approccio che possiamo adottare è applicare trasformazioni 3D agli elementi. In questa situazione, agli elementi con le trasformazioni 3D applicate viene assegnato un nuovo livello per elemento e, nei browser WebKit, spesso si verifica anche il passaggio al compositore hardware. Nell'opzione 1, invece, avevamo un unico livello grande per la pagina che doveva essere ridisegnato ogni volta che cambiava qualcosa e tutta la pittura e il compositing erano gestiti dalla CPU.

Ciò significa che con questa opzione le cose sono diverse: abbiamo potenzialmente un livello per ogni elemento a cui applichiamo una trasformazione 3D. Se da questo punto in poi non facciamo altro che applicare altre trasformazioni agli elementi, non dovremo ridipingere il livello e la GPU potrà occuparsi di spostare gli elementi e comporre la pagina finale.

Spesso le persone utilizzano semplicemente l'hack -webkit-transform: translateZ(0); e notano miglioramenti magici delle prestazioni. Anche se questa soluzione funziona oggi, ci sono dei problemi:

  1. Non è compatibile con più browser.
  2. Forza il browser a creare un nuovo livello per ogni elemento trasformato. Troppi livelli possono causare altri colli di bottiglia delle prestazioni, quindi utilizzali con parsimonia.
  3. È stata disattivata per alcune porte WebKit (quarto punto elenco da sotto).

Se scegli la strada della traduzione 3D, fai attenzione: è una soluzione temporanea al problema. Idealmente, le trasformazioni 2D dovrebbero avere caratteristiche di rendering simili a quelle 3D. I browser stanno progredendo a un ritmo eccezionale, quindi speriamo di vedere questo prima.

Infine, cerca di evitare le vernici, se possibile, e sposta semplicemente gli elementi esistenti all'interno della pagina. A titolo esemplificativo, è un approccio tipico dei siti con parallasse utilizzare div con altezza fissa e modificare la posizione dello sfondo per ottenere l'effetto. Purtroppo, ciò significa che l'elemento deve essere ridisegnato a ogni passaggio, il che può comportare un costo in termini di prestazioni. Se possibile, dovresti creare l'elemento (se necessario, inseriscilo in un div con overflow: hidden) e tradurlo semplicemente.

Opzione 3: utilizza un canvas o WebGL con posizione fissa

L'ultima opzione che prenderemo in considerazione è l'utilizzo di un canvas con posizione fissa nella parte posteriore della pagina in cui disegneremo le immagini trasformate. A prima vista potrebbe non sembrare la soluzione più efficace, ma questo approccio presenta alcuni vantaggi:

  • Non è più necessario tanto lavoro di composizione perché abbiamo un solo elemento, la tela.
  • In pratica, abbiamo a che fare con un singolo bitmap con accelerazione hardware.
  • L'API Canvas2D è perfetta per il tipo di trasformazioni che vogliamo eseguire, il che significa che lo sviluppo e la manutenzione sono più gestibili.

L'utilizzo di un elemento canvas ci offre un nuovo livello, ma è solo uno, mentre nell'opzione 2 ci è stato effettivamente fornito un nuovo livello per ogni elemento a cui è stata applicata una trasformazione 3D, quindi abbiamo un carico di lavoro maggiore per la composizione di tutti questi livelli. Si tratta anche della soluzione più compatibile oggi disponibile alla luce delle diverse implementazioni delle trasformazioni tra browser.


/**
 * Updates and draws in the underlying visual elements to the canvas.
 */
function updateElements () {

  var relativeY = lastScrollY / h;

  // Fill the canvas up
  context.fillStyle = "#1e2124";
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the background
  context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));

  // Draw each of the blobs in turn
  context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
  context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
  context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
  context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
  context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
  context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
  context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
  context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
  context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));

  // Allow another rAF call to be scheduled
  ticking = false;
}

/**
 * Calculates a relative disposition given the page's scroll
 * range normalized from 0 to 1
 * @param {number} base The starting value.
 * @param {number} range The amount of pixels it can move.
 * @param {number} relY The normalized scroll value.
 * @param {number} offset A base normalized value from which to start the scroll behavior.
 * @returns {number} The updated position value.
 */
function pos(base, range, relY, offset) {
  return base + limit(0, 1, relY - offset) * range;
}

/**
 * Clamps a number to a range.
 * @param {number} min The minimum value.
 * @param {number} max The maximum value.
 * @param {number} value The value to limit.
 * @returns {number} The clamped value.
 */
function limit(min, max, value) {
  return Math.max(min, Math.min(max, value));
}

Questo approccio funziona davvero quando si hanno a che fare con immagini di grandi dimensioni (o altri elementi che possono essere facilmente scritti in un canvas) e, certamente, gestire blocchi di testo di grandi dimensioni sarebbe più complicato, ma, a seconda del sito, potrebbe rivelarsi la soluzione più appropriata. Se devi gestire il testo nel canvas, devi utilizzare il metodo dell'API fillText, ma a costo dell'accessibilità (hai appena rasterizzato il testo in una bitmap!) e ora dovrai gestire il rientro a capo e una serie di altri problemi. Se puoi evitarlo, ti consigliamo di farlo e probabilmente ti conviene utilizzare l'approccio di trasformazione descritto sopra.

Dato che stiamo cercando di ottenere il massimo possibile, non c'è motivo di presumere che la parallasse debba essere eseguita all'interno di un elemento canvas. Se il browser lo supporta, potremmo utilizzare WebGL. Il punto chiave è che WebGL ha il percorso più diretto di tutte le API per la scheda grafica e, come tale, è la soluzione più probabile per raggiungere i 60 fps, soprattutto se gli effetti del sito sono complessi.

La tua reazione immediata potrebbe essere che WebGL è eccessivo o che non è onnipresente in termini di supporto, ma se utilizzi qualcosa come Three.js, puoi sempre ricorrere all'utilizzo di un elemento canvas e il codice viene astratto in modo coerente e intuitivo. Non dobbiamo fare altro che utilizzare Modernizr per verificare il supporto dell'API appropriato:

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

Come ultima considerazione su questo approccio, se non ti piace molto aggiungere elementi extra alla pagina, puoi sempre utilizzare una tela come elemento di sfondo sia in Firefox che nei browser basati su WebKit. Ovviamente non è una situazione generalizzata, quindi come sempre devi prestare attenzione.

A te la scelta

Il motivo principale per cui gli sviluppatori utilizzano per impostazione predefinita gli elementi con posizionamento assoluto anziché altre opzioni potrebbe essere semplicemente l'ubiquità del supporto. Ciò è, in qualche misura, illusorio, poiché i browser meno recenti scelti come target potrebbero offrire un'esperienza di rendering estremamente scadente. Anche nei browser moderni di oggi, l'utilizzo di elementi con posizionamento assoluto non comporta necessariamente un buon rendimento.

Le trasformazioni, in particolare quelle 3D, ti offrono la possibilità di lavorare direttamente con gli elementi DOM e di ottenere una frequenza fotogrammi stabile. La chiave per ottenere risultati ottimali è evitare di dipingere ovunque sia possibile e provare semplicemente a spostare gli elementi. Tieni presente che il modo in cui i browser WebKit creano i livelli non è necessariamente correlato ad altri motori di browser, quindi assicurati di testarlo prima di impegnarti a utilizzare questa soluzione.

Se vuoi raggiungere solo il livello superiore dei browser e sei in grado di eseguire il rendering del sito utilizzando i canvas, questa potrebbe essere la soluzione migliore per te. Se utilizzi Three.js, dovresti essere in grado di passare da un renderer all'altro molto facilmente, a seconda del supporto di cui hai bisogno.

Conclusione

Abbiamo valutato alcuni approcci per gestire i siti con effetto parallasse, dagli elementi con posizionamento assoluto all'utilizzo di un canvas con posizione fissa. L'implementazione che scegli dipenderà, ovviamente, da cosa stai cercando di ottenere e dal design specifico con cui stai lavorando, ma è sempre bene sapere che hai delle opzioni.

Come sempre, qualunque approccio tu scelga, non indovinare, testa.