Miglioramento del rendimento di Canvas HTML5

Boris Smus
Boris Smus

Introduzione

HTML5 canvas, nato come esperimento di Apple, è lo standard più supportato per le grafica in modalità immediata 2D sul web. Ora molti sviluppatori si affidano a questa tecnologia per una vasta gamma di progetti multimediali, visualizzazioni e giochi. Tuttavia, con l'aumentare della complessità delle applicazioni che sviluppiamo, gli sviluppatori si scontrano inavvertitamente con il muro del rendimento. Esistono molte conoscenze disconnesse sull'ottimizzazione delle prestazioni dei canvas. Questo articolo mira a consolidare parte di questo corpo in una risorsa più facilmente fruibile per gli sviluppatori. Questo articolo include ottimizzazioni fondamentali che si applicano a tutti gli ambienti di computer graphics, nonché tecniche specifiche per Canvas soggette a modifiche man mano che le implementazioni di Canvas migliorano. In particolare, man mano che i fornitori di browser implementano l'accelerazione della GPU di Canvas, alcune delle tecniche di miglioramento delle prestazioni descritte potrebbero diventare meno efficaci. Lo comunicheremo dove opportuno. Tieni presente che questo articolo non riguarda l'utilizzo del canvas HTML5. Per farlo, consulta questi articoli correlati a Canvas su HTML5Rocks, questo capitolo del sito Dive into HTML5 o il tutorial MDN Canvas.

Test delle prestazioni

Per far fronte al mondo in rapida evoluzione di HTML5 canvas, i test di JSPerf (jsperf.com) verificano che ogni ottimizzazione proposta funzioni ancora. JSPerf è un'applicazione web che consente agli sviluppatori di scrivere test di prestazioni JavaScript. Ogni test si concentra su un risultato che stai cercando di ottenere (ad esempio, svuotare la tela) e include più approcci che ottengono lo stesso risultato. JSPerf esegue ogni approccio il maggior numero di volte possibile in un breve periodo di tempo e fornisce un numero statisticamente significativo di iterazioni al secondo. I punteggi più alti sono sempre i migliori. I visitatori di una pagina di test di prestazioni di JSPerf possono eseguire il test sul proprio browser e consentire a JSPerf di memorizzare i risultati del test normalizzati su Browserscope (browserscope.org). Poiché le tecniche di ottimizzazione descritte in questo articolo sono supportate da un risultato JSPerf, puoi tornare a visualizzare informazioni aggiornate sull'eventuale applicazione della tecnica. Ho scritto una piccola applicazione di supporto che visualizza questi risultati sotto forma di grafici, incorporati in questo articolo.

Tutti i risultati relativi alle prestazioni in questo articolo si basano sulla versione del browser. Questo rappresenta un limite, poiché non sappiamo su quale sistema operativo era in esecuzione il browser o, ancora più importante, se la funzionalità HTML5 canvas era o meno accelerata hardware al momento dell'esecuzione del test delle prestazioni. Puoi scoprire se la tela HTML5 di Chrome è con accelerazione hardware visitando about:gpu nella barra degli indirizzi.

Esegui il pre-rendering in una tela off-screen

Se ridisegni primitive simili sullo schermo in più frame, come spesso accade quando scrivi un gioco, puoi ottenere grandi miglioramenti delle prestazioni eseguendo il pre-rendering di ampie parti della scena. Il pre-rendering consiste nell'utilizzare una o più canvas off-screen su cui eseguire il rendering di immagini temporanee, per poi eseguire nuovamente il rendering delle canvas off-screen su quella visibile. Ad esempio, supponiamo che tu stia ridisegnando Mario che corre a 60 fotogrammi al secondo. Puoi ridisegnare il cappello, i baffi e la "M" a ogni frame oppure eseguire il prerendering di Mario prima di eseguire l'animazione. nessun pre-rendering:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

pre-rendering:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext('2d');
drawMario(m_context);

function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

Nota l'utilizzo di requestAnimationFrame, di cui parleremo più dettagliatamente in una sezione successiva.

Questa tecnica è particolarmente efficace quando l'operazione di rendering (drawMario nell'esempio precedente) è costosa. Un buon esempio è il rendering del testo, un'operazione molto costosa.

Tuttavia, il rendimento scadente del caso di test "pre-rendering libero". Durante il pre-rendering, è importante assicurarsi che la tela temporanea si adatti perfettamente all'immagine che stai disegnando, altrimenti il guadagno in termini di prestazioni del rendering off-screen viene compensato dalla perdita di prestazioni dovuta alla copia di una tela grande su un'altra (che varia in funzione delle dimensioni del target di origine). Un canvas aderente nel test riportato sopra è semplicemente più piccolo:

can2.width = 100;
can2.height = 40;

Rispetto a quella più generica che offre prestazioni più scarse:

can3.width = 300;
can3.height = 100;

Raggruppare le chiamate al canvas

Poiché il disegno è un'operazione costosa, è più efficiente caricare la macchina a stati di disegno con un lungo set di comandi e quindi fare in modo che li scarichi tutti nel buffer video.

Ad esempio, quando si tracciano più linee, è più efficiente creare un percorso con tutte le linee al suo interno e disegnarlo con un'unica chiamata di disegno. In altre altre parole, anziché disegnare linee separate:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

Otteniamo prestazioni migliori disegnando una singola polilinea:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

Questo vale anche per il canvas HTML5. Ad esempio, quando disegni un percorso complesso, è meglio inserire tutti i punti nel percorso anziché eseguire il rendering dei segmenti separatamente (jsperf).

Tieni presente, però, che con Canvas esiste un'eccezione importante a questa regola: se le primitive coinvolte nel disegno dell'oggetto desiderato hanno caselle delimitanti piccole (ad esempio linee orizzontali e verticali), potrebbe essere più efficiente visualizzarle separatamente (jsperf).

Evita modifiche non necessarie dello stato del canvas

L'elemento canvas HTML5 è implementato su una macchina a stati che monitora elementi come gli stili di riempimento e tratto, nonché i punti precedenti che compongono il percorso corrente. Quando si cerca di ottimizzare il rendimento della grafica, è facile concentrarsi solo sul rendering della grafica. Tuttavia, la manipolazione della macchina a stati può comportare anche un overhead delle prestazioni. Ad esempio, se utilizzi più colori di riempimento per il rendering di una scena, è più economico eseguire il rendering in base al colore anziché in base al posizionamento nell'area di disegno. Per eseguire il rendering di un motivo a righe, puoi eseguire il rendering di una riga, cambiare i colori, eseguire il rendering della riga successiva e così via:

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

In alternativa, puoi visualizzare tutte le strisce dispari e poi tutte quelle pari:

context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

Come previsto, l'approccio interlacciato è più lento perché la modifica della macchina a stati è costosa.

Mostra solo le differenze tra le schermate, non l'intero nuovo stato

Come è facile immaginare, eseguire meno rendering sullo schermo è più economico rispetto a eseguire più rendering. Se hai solo differenze incrementali tra i rielaborazioni, puoi ottenere un aumento significativo del rendimento semplicemente disegnando la differenza. In altre parole, anziché cancellare l'intero schermo prima di iniziare a disegnare:

context.fillRect(0, 0, canvas.width, canvas.height);

Tieni traccia del riquadro di delimitazione disegnato e cancella solo quello.

context.fillRect(last.x, last.y, last.width, last.height);

Se hai dimestichezza con la computer grafica, potresti anche conoscere questa tecnica come "regioni di ridisegnare", in cui la regione delimitante precedentemente visualizzata viene salvata e poi cancellata a ogni rendering. Questa tecnica si applica anche ai contesti di rendering basati su pixel, come illustrato da questo linguaggio dell'emulatore di NintendoNintendo JavaScript.

Utilizzare più canvas a livelli per scene complesse

Come accennato in precedenza, il disegno di immagini di grandi dimensioni è costoso e deve essere evitato, se possibile. Oltre a utilizzare un altro canvas per il rendering fuori schermo, come illustrato nella sezione di pre-rendering, possiamo anche utilizzare canvas sovrapposti uno sull'altro. Utilizzando la trasparenza nella canvas di primo piano, possiamo fare affidamento sulla GPU per comporre gli alfa insieme al momento del rendering. Potresti configurare la scena nel seguente modo, con due canvas con posizionamento assoluto, uno sopra l'altro.

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

Il vantaggio rispetto a un solo canvas è che quando disegniamo o svuotiamo il canvas in primo piano, non modifichiamo mai lo sfondo. Se il tuo gioco o la tua app multimediale può essere suddiviso in primo piano e sfondo, valuta la possibilità di eseguire il rendering su canvas distinti per ottenere un notevole miglioramento delle prestazioni.

Spesso puoi sfruttare la percezione umana imperfetta e eseguire il rendering dell'immagine di sfondo una sola volta o a una velocità inferiore rispetto all'immagine in primo piano (che probabilmente occuperà la maggior parte dell'attenzione dell'utente). Ad esempio, puoi eseguire il rendering del primo piano ogni volta che esegui il rendering, ma eseguire il rendering dell'altro solo ogni N fotogrammi. Tieni inoltre presente che questo approccio si generalizza bene per qualsiasi numero di canvas compositi se la tua applicazione funziona meglio con questo tipo di struttura.

Evita shadowBlur

Come molti altri ambienti grafici, HTML5 canvas consente agli sviluppatori di smussare le primitive, ma questa operazione può essere molto costosa:

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

Conoscere diversi modi per cancellare la tela

Poiché la tela HTML5 è un paradigma di disegno in modalità immediata, la scena deve essere ridisegnata esplicitamente in ogni frame. Per questo motivo, la cancellazione del canvas è un'operazione di fondamentale importanza per le app e i giochi canvas HTML5. Come accennato nella sezione Evitare le modifiche dello stato del canvas, spesso non è consigliabile cancellare l'intero canvas, ma se devi farlo, hai due opzioni: chiamare context.clearRect(0, 0, width, height) o utilizzare un hack specifico per il canvas: canvas.width = canvas.width. Al momento della stesura di questo articolo, clearRect in genere supera la versione con reimpostazione della larghezza, ma in alcuni casi l'utilizzo dell'hack di reimpostazione canvas.width è notevolmente più veloce in Chrome 14

Fai attenzione a questo suggerimento, poiché dipende fortemente dall'implementazione del canvas sottostante ed è soggetto a molte modifiche. Per ulteriori informazioni, leggi l'articolo di Simon Sarris sulla cancellazione della tela.

Evita coordinate in virgola mobile

Il canvas HTML5 supporta il rendering a livello di subpixel e non è possibile disattivarlo. Se disegni con coordinate che non sono numeri interi, viene utilizzato automaticamente l'anti-aliasing per cercare di smussare le linee. Ecco l'effetto visivo, tratto da questo articolo sul rendimento della tela con pixel secondari di Seb Lee-Delisle:

Subpixel

Se lo sprite smussato non è l'effetto che cerchi, può essere molto più veloce convertire le coordinate in numeri interi utilizzando Math.floor o Math.round (jsperf):

Per convertire le coordinate in virgola mobile in numeri interi, puoi utilizzare diverse tecniche intelligenti, la più efficace delle quali consiste nell'aggiungere metà al numero di destinazione e poi eseguire operazioni bit a bit sul risultato per eliminare la parte frazionaria.

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

Puoi trovare la suddivisione completa del rendimento qui (jsperf).

Tieni presente che questo tipo di ottimizzazione non dovrebbe più essere importante una volta che le implementazioni di canvas saranno accelerate dalla GPU, in quanto potranno eseguire rapidamente il rendering di coordinate non intere.

Ottimizzare le animazioni con requestAnimationFrame

La relativamente nuova API requestAnimationFrame è il metodo consigliato per implementare applicazioni interattive nel browser. Anziché ordinare al browser di eseguire il rendering a una determinata frequenza di aggiornamento fissa, chiedi educatamente al browser di chiamare la tua routine di rendering e di chiamarti quando il browser è disponibile. Come piacevole effetto collaterale, se la pagina non è in primo piano, il browser è abbastanza intelligente da non eseguire il rendering. Il callback requestAnimationFrame mira a una percentuale di callback di 60 f/s, ma non lo garantisce, quindi devi tenere traccia del tempo trascorso dall'ultimo rendering. L'URL può avere il seguente aspetto:

var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
  var delta = Date.now() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

Tieni presente che questo utilizzo di requestAnimationFrame si applica a canvas e ad altre tecnologie di rendering come WebGL. Al momento della stesura di questo articolo, questa API è disponibile solo in Chrome, Safari e Firefox, quindi ti consigliamo di utilizzare questo shim.

La maggior parte delle implementazioni di canvas mobile è lenta

Parliamo di dispositivi mobili. Purtroppo, al momento della stesura di questo documento, solo iOS 5.0 beta con Safari 5.1 ha un'implementazione di canvas mobile con accelerazione GPU. Senza l'accelerazione GPU, i browser mobile in genere non hanno CPU sufficientemente potenti per le applicazioni moderne basate su canvas. Alcuni dei test JSPerf descritti sopra hanno un ordine di grandezza peggiore sui dispositivi mobili rispetto ai computer, limitando notevolmente i tipi di app cross-device che puoi eseguire correttamente.

Conclusione

In sintesi, questo articolo ha trattato un insieme completo di tecniche di ottimizzazione utili che ti aiuteranno a sviluppare progetti basati su canvas HTML5 di alto rendimento. Ora che hai imparato qualcosa di nuovo, vai e ottimizza le tue fantastiche creazioni. In alternativa, se al momento non hai un gioco o un'applicazione da ottimizzare, dai un'occhiata a Chrome Experiments e Creative JS per trovare ispirazione.

Riferimenti