Parallasse'

Paul Lewis

Introduzione

I siti con parallasse sono di gran moda ultimamente, dai un'occhiata a questi:

Se non li conosci, sono i siti in cui la struttura visiva della pagina cambia mentre scorri. Di solito, gli elementi all'interno della pagina vengono ridimensionati, ruotano o si spostano proporzionalmente alla posizione di scorrimento sulla pagina.

Una pagina demo Parallasse
La nostra pagina demo completa dell'effetto parallasse

Che ti piaccia o meno la parallasse dei siti è una cosa, ma quello che puoi dire con certezza è che si tratta di un "buco nero" in termini di rendimento. Il motivo è che i browser tendono a essere ottimizzati per i casi in cui nuovi contenuti vengono visualizzati nella parte superiore o inferiore dello schermo quando scorri (in base alla direzione di scorrimento) e, in termini generali, i browser funzionano meglio quando cambia molto poco durante lo scorrimento. Questo è raramente il caso di un sito con parallasse, dal momento che gli elementi visivi di grandi dimensioni che si trovano in tutta la pagina cambiano e il browser ridipinge l'intera pagina.

È ragionevole generalizzare un sito con parallasse come questo:

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

In precedenza abbiamo parlato delle prestazioni dello scorrimento e dei modi in cui puoi migliorare la reattività dell'app. Questo articolo si basa su queste basi, pertanto ti consigliamo di leggerlo, se non lo hai già fatto.

La domanda è: se stai creando un sito a scorrimento con parallasse, sei vincolato a ridipingere costose o ci sono approcci alternativi che puoi adottare per massimizzare le prestazioni? Diamo un'occhiata alle opzioni a nostra disposizione.

Opzione 1: utilizzare elementi DOM e posizioni assolute

Questo sembra essere l'approccio predefinito adottato dalla maggior parte delle persone. La pagina contiene diversi elementi e, ogni volta che viene attivato un evento di scorrimento, vengono eseguiti diversi aggiornamenti visivi per trasformarli.

Se avvii la sequenza temporale di DevTools in modalità frame e scorri intorno, noterai che è necessario eseguire costose operazioni di colorazione a schermo intero; se scorri molto spesso, potresti vedere diversi eventi di scorrimento all'interno di un singolo frame, ognuno dei quali attiverà il funzionamento del layout.

Chrome DevTools senza eventi di scorrimento eliminati.
DevTools mostra colori di grandi dimensioni e più layout attivati da eventi in un unico frame.

La cosa importante da tenere a mente è che per raggiungere i 60 fps (corrispondente alla tipica frequenza di aggiornamento del monitor di 60 Hz) abbiamo poco più di 16 ms per fare tutto. In questa prima versione stiamo eseguendo i nostri aggiornamenti visivi ogni volta che riceviamo un evento di scorrimento, ma, come abbiamo già detto negli articoli precedenti su animazioni più sottili e più clandestine con requestAnimationFrame e prestazioni di scorrimento, ciò non coincide con il programma di aggiornamento del browser, quindi perdiamo frame o facciamo troppo lavoro all'interno di ciascuno. Questo potrebbe causare facilmente un aspetto sgradevole e innaturale sul tuo sito, il che potrebbe generare delusioni e gattini infelici.

Spostiamo il codice di aggiornamento dall'evento di scorrimento a un callback requestAnimationFrame e acquisisci 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 che attiviamo con lo scorrimento non è così costosa, ma in altri casi d'uso potrebbe esserlo. Ora eseguiamo almeno un'operazione di layout in ogni frame.

Chrome DevTools con eventi di scorrimento esclusi.
DevTools mostra colori di grandi dimensioni e più layout attivati da eventi in un unico frame.

Ora possiamo gestire uno o cento eventi di scorrimento per frame, ma soprattutto memorizziamo il valore più recente per utilizzarlo ogni volta che il callback requestAnimationFrame viene eseguito ed esegue i nostri aggiornamenti visivi. Il punto è che sei passato dal tentativo di forzare gli aggiornamenti visivi ogni volta che ricevi un evento di scorrimento alla richiesta di fornire al browser una finestra appropriata in cui eseguirli. Non sei dolce?

Il problema principale di questo approccio, requestAnimationFrame o meno, è che abbiamo essenzialmente un livello per l'intera pagina e spostando questi elementi visivi dobbiamo ridipingere di grandi dimensioni (e costose). In genere, il disegno è un'operazione di blocco (sebbene stia cambiando), il che significa che il browser non può svolgere altre operazioni e spesso esauriamo enormemente il budget del nostro frame, che è di 16 ms, e le cose rimangono inadeguate.

Opzione 2: utilizza elementi DOM e trasformazioni 3D

Invece di utilizzare posizioni assolute, un altro approccio che possiamo assumere è applicare le trasformazioni 3D agli elementi. In questa situazione vediamo che agli elementi con le trasformazioni 3D applicate viene assegnato un nuovo livello per elemento e, nei browser WebKit, spesso ciò causa anche un passaggio al compositor hardware. Nell'opzione 1, al contrario, avevamo un unico livello di grandi dimensioni per la pagina che doveva essere ridipinto quando qualcosa cambiava e tutto il disegno e la composizione erano gestiti dalla CPU.

Ciò significa che con questa opzione, le cose sono diverse: potenzialmente abbiamo un livello per ogni elemento a cui applichiamo una trasformazione 3D. Da questo momento in poi, invece, facciamo più trasformazioni sugli elementi, non c'è bisogno di ridipingere il livello e la GPU è in grado di spostare gli elementi e di comporre insieme la pagina finale.

Molte volte le persone utilizzano semplicemente la prova di abilità -webkit-transform: translateZ(0); per vedere miglioramenti delle prestazioni magici e, anche se oggi funziona, si verificano dei problemi:

  1. Non è compatibile con più browser.
  2. Forza la mano del browser creando un nuovo livello per ogni elemento trasformato. Molti strati possono causare altri colli di bottiglia, quindi usalo con parsimonia.
  3. È stato disattivato per alcune porte WebKit (il quarto punto in basso dal basso).

Se segui il percorso di traduzione 3D fai attenzione, si tratta di una soluzione temporanea al tuo problema. Idealmente, potremmo vedere caratteristiche di rendering simili nelle trasformazioni 2D a quelle del 3D. I browser stanno progredendo a una velocità fenomenale, quindi speriamo che prima questo sia ciò che vedremo a vedere.

Infine, cerca di evitare di colorare la pagina ovunque sia possibile e di spostare semplicemente gli elementi esistenti nella pagina. Ad esempio, nei siti con parallasse è un approccio tipico utilizzare tag div ad altezza fissa e modificare la posizione dello sfondo per ottenere l'effetto. Purtroppo, ciò significa che l'elemento deve essere riverniciato a ogni passaggio, il che può comportare un costo in termini di prestazioni. Se possibile, dovresti invece creare l'elemento (avvolgilo all'interno di un div con overflow: hidden se necessario) e tradurlo semplicemente.

Opzione 3: utilizza un canvas in posizione fissa o WebGL

L'ultima opzione che prenderemo in considerazione è l'utilizzo di una tela con posizione fissa sul retro della pagina in cui disegnare le immagini trasformate. A prima vista potrebbe non sembrare la soluzione più efficiente, ma in realtà questo approccio offre alcuni vantaggi:

  • Non abbiamo più bisogno di molto lavoro da parte del compositore perché ha un solo elemento, il canvas.
  • Abbiamo a che fare con una singola bitmap con accelerazione hardware.
  • L'API Canvas2D è perfetta per il tipo di trasformazioni che stiamo cercando di eseguire, il che significa che lo sviluppo e la manutenzione sono più gestibili.

L'uso di un elemento canvas ci dà un nuovo livello, ma è solo un livello, mentre nell'opzione 2 ci è stato fornito un nuovo livello per ogni elemento a cui è stata applicata una trasformazione 3D, quindi abbiamo un aumento del carico di lavoro che combina tutti questi livelli. Questa è anche la soluzione attualmente più compatibile alla luce delle diverse implementazioni cross-browser delle trasformazioni.


/**
 * 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 hai a che fare con immagini di grandi dimensioni (o altri elementi che possono essere facilmente scritti su un canvas) e sicuramente gestire grandi blocchi di testo sarebbe più impegnativo, ma a seconda del tuo sito potrebbe rivelarsi la soluzione più appropriata. Se hai a che fare con il testo nel canvas, dovresti usare il metodo dell'API fillText, ma è a scapito dell'accessibilità (hai appena rasterizzato il testo in una bitmap) e ora dovrai gestire il ritorno a capo e tutta una serie di altri problemi. Se puoi evitarlo, dovresti davvero e probabilmente riceveresti un servizio migliore utilizzando l'approccio delle trasformazioni riportato sopra.

Poiché stiamo portando questo aspetto il più lontano possibile, non c'è motivo di presumere che l'opera con parallasse debba essere svolta all'interno di un elemento canvas. Se il browser lo supporta, possiamo usare WebGL. La chiave qui è che WebGL ha il routing più diretto di tutte le API alla scheda grafica e, come tale, è il candidato 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 a un elemento canvas per astrarre il codice in modo coerente e amichevole. Non dobbiamo fare altro che usare 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();
}

Per riassumere, se non ti piace aggiungere ulteriori elementi alla pagina, puoi sempre utilizzare una tela come elemento di sfondo sia nei browser Firefox che in quelli basati su WebKit. Ciò non è onnipresente, ovviamente, quindi come al solito dovresti trattarlo con cautela.

Sta a te scegliere

Il motivo principale per cui gli sviluppatori impostano in modo predefinito elementi posizionati in modo assoluto piuttosto che altre opzioni, potrebbe essere semplicemente l'ubiquità del supporto. Questo è, in una certa misura, illusorio, poiché i browser meno recenti presi di mira potrebbero fornire un'esperienza di rendering estremamente scadente. Anche nei browser moderni odierni, l'utilizzo di elementi assolutamente posizionati non sempre comporta un buon rendimento.

Le trasformazioni, sicuramente di tipo 3D, offrono la possibilità di lavorare direttamente con gli elementi DOM e di ottenere una frequenza fotogrammi stabile. La chiave del successo in questo caso è evitare di dipingere ovunque sia possibile e semplicemente provare a spostare gli elementi da una parte all'altra. Tieni presente che il modo in cui i browser WebKit creano i livelli non è necessariamente correlato ad altri motori del browser, quindi assicurati di testarlo prima di impegnarti per questa soluzione.

Se punti solo ai browser di primo livello e sei in grado di eseguire il rendering del sito utilizzando canvas, questa potrebbe essere l'opzione migliore per te. Ovviamente se utilizzi Three.js dovresti essere in grado di scambiare e passare da un renderer all'altro molto facilmente, a seconda del supporto richiesto.

Conclusione

Abbiamo valutato alcuni approcci alla gestione dei siti con parallasse, da elementi posizionati in modo assoluto all'utilizzo di canvas con posizione fissa. L'implementazione da parte tua dipende, ovviamente, dall'obiettivo che vuoi raggiungere e dal design specifico con cui lavori, ma è sempre bene sapere di avere a disposizione diverse opzioni.

E, come sempre, qualunque sia l'approccio che provi: non indovinare, prova.