Miglioramento del rendimento di Canvas HTML5

Boris Smus
Boris Smus

Introduzione

La canvas HTML5, nata come esperimento di Apple, è lo standard più supportato per la grafica in modalità immediata 2D sul web. Molti sviluppatori lo usano per creare progetti, visualizzazioni e giochi multimediali. Tuttavia, poiché le applicazioni che creiamo aumentano in complessità, gli sviluppatori hanno inavvertitamente raggiunto il muro delle prestazioni. C'è molta intelligenza sull'ottimizzazione delle prestazioni delle canvas. Questo articolo ha lo scopo di consolidare parte del testo in una risorsa più facilmente consultabile per gli sviluppatori. Questo articolo include ottimizzazioni di base che si applicano a tutti gli ambienti di computer grafica, nonché tecniche specifiche per canvas che sono soggette a modifiche man mano che le implementazioni canvas migliorano. In particolare, man mano che i fornitori di browser implementano l'accelerazione GPU canvas, alcune delle tecniche di performance descritte menzionate avranno probabilmente meno impatto. Se necessario, verrà indicato questo aspetto. Tieni presente che questo articolo non fa riferimento all'utilizzo del canvas HTML5. Consulta questi articoli correlati alle canvas su HTML5Rocks, questo capitolo sul sito Dive into HTML5 o il tutorial su MDN Canvas.

Test delle prestazioni

Per affrontare il mondo in rapida evoluzione del canvas HTML5, i test JSPerf (jsperf.com) verificano che ogni ottimizzazione proposta continui a funzionare. JSPerf è un'applicazione web che consente agli sviluppatori di scrivere test delle prestazioni JavaScript. Ogni test è incentrato sul risultato che stai cercando di ottenere (ad esempio, l'eliminazione del canvas) e include diversi approcci che ottengono lo stesso risultato. JSPerf esegue ciascun approccio il maggior numero possibile di volte in un breve periodo di tempo e fornisce un numero statisticamente significativo di iterazioni al secondo. I punteggi più alti sono sempre migliori. I visitatori di una pagina di test delle prestazioni JSPerf possono eseguire il test sul proprio browser e lasciare che JSPerf memorizzi 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 visualizzare informazioni aggiornate che indicano se la tecnica è ancora valida. Ho scritto una piccola applicazione helper che visualizza questi risultati come grafici, incorporati in questo articolo.

Tutti i risultati relativi alle prestazioni in questo articolo sono associati alla versione del browser. Questo si rivela una limitazione, dal momento che non conosciamo il sistema operativo su cui era in esecuzione il browser o, ancora più importante, se la tecnologia canvas HTML5 è stata accelerata hardware al momento dell'esecuzione del test delle prestazioni. Puoi scoprire se il canvas HTML5 di Chrome è con accelerazione hardware visitando about:gpu nella barra degli indirizzi.

Esegui il pre-rendering su una tela fuori schermo

Se stai ridisegnando primitive simili sullo schermo attraverso più frame, come spesso accade quando scrivi un gioco, puoi ottenere grandi miglioramenti in termini di prestazioni eseguendo il pre-rendering di ampie parti della scena. Il pre-rendering prevede l'utilizzo di canvas (o canvas) fuori schermo separati su cui eseguire il rendering delle immagini temporanee, per poi eseguire il rendering delle tele fuori schermo su quella visibile. Ad esempio, supponiamo che tu stia ridisegnando Mario a 60 frame al secondo. Puoi ridisegnare il cappello, i baffi e la "M" a ogni fotogramma, oppure eseguire il pre-rendering di Mario prima di avviare l'animazione. senza 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, che verrà discusso più dettagliatamente in una sezione successiva.

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

Tuttavia, le prestazioni scadenti dello scenario di test "loop pre-rendering". Durante il pre-rendering, è importante assicurarti che il canvas temporaneo si adatti perfettamente all'immagine che stai disegnando, altrimenti il guadagno delle prestazioni del rendering fuori schermo è controbilanciato dalla perdita in termini di prestazioni della copia di un canvas di grandi dimensioni su un altro (che varia in funzione delle dimensioni di destinazione dell'origine). Una tela aderente nel test precedente è semplicemente più piccolo:

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

Rispetto a quello sciolto che genera prestazioni più scarse:

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

Chiamate canvas batch insieme

Poiché il disegno è un'operazione costosa, è più efficiente caricare la macchina a stati di disegno con una lunga serie di comandi e poi scaricarli tutti nel buffer video.

Ad esempio, quando si tracciano più linee, è più efficiente creare un unico percorso con tutte le linee e tracciarlo con un'unica chiamata di disegno. In altre parole, anziché tracciare 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();
}

Disegnando una singola polilinea si ottengono prestazioni migliori:

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 le canvas HTML5. Ad esempio, quando tracci un percorso complesso, è meglio inserire tutti i punti al suo interno, anziché eseguire il rendering dei segmenti separatamente (jsperf).

Tuttavia, tieni presente che con Canvas c'è un'importante eccezione a questa regola: se le primitive coinvolte nel disegno dell'oggetto desiderato hanno riquadri di delimitazione piccoli (ad esempio linee orizzontali e verticali), potrebbe essere più efficiente eseguirne il rendering separato (jsperf).

Evita modifiche non necessarie dello stato del canvas

L'elemento canvas HTML5 viene implementato su una macchina a stato che tiene traccia di elementi come gli stili di riempimento e tratto, nonché i punti precedenti che compongono il percorso corrente. Quando si cerca di ottimizzare le prestazioni delle grafiche, si è tentati di concentrarsi esclusivamente sul rendering grafico. Tuttavia, manipolare la 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 per colore anziché per posizionamento sul canvas. Per visualizzare un motivo gessato, puoi eseguire il rendering di una striscia, cambiare i colori, la striscia successiva e così via:

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

Oppure visualizza tutte le strisce dispari e poi tutte le strisce 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é cambiare la macchina a stato è costoso.

Visualizza solo le differenze dello schermo, non lo stato completamente nuovo

Come ci si aspetterebbe, visualizzare meno sullo schermo è più economico che visualizzare di più. Se noti solo differenze incrementali tra le modifiche, puoi ottenere un miglioramento significativo delle prestazioni semplicemente tracciando la differenza. In altre parole, anziché cancellare l'intero schermo prima di disegnare:

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

Tieni traccia del riquadro di delimitazione disegnato e cancellalo.

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

Se hai familiarità con la grafica al computer, potresti anche usare questa tecnica come "Ridisegna regioni", in cui il riquadro di delimitazione visualizzato in precedenza viene salvato e poi cancellato a ogni rendering. Questa tecnica si applica anche ai contesti di rendering basati su pixel, come illustrato in Nintendo emulator talk in JavaScript.

Usare più tele a livelli per scene complesse

Come accennato prima, disegnare immagini di grandi dimensioni è costoso e, se possibile, è meglio evitare. Oltre a utilizzare un'altra tela per eseguire il rendering all'esterno dello schermo, come illustrato nella sezione di pre-rendering, possiamo utilizzare anche canvas sovrapposti. Grazie alla trasparenza nel canvas in primo piano, possiamo fare affidamento sulla GPU per comporre le versioni alpha insieme al momento del rendering. Puoi configurarlo nel seguente modo, con due canvas posizionati 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 all'uso di un solo canvas è che quando disegniamo o cancelliamo il canvas in primo piano, non lo modifichiamo mai. Se il tuo gioco o la tua app multimediale possono essere suddivisi in primo piano e in background, valuta la possibilità di eseguire il rendering di questi elementi su canvas separati per ottenere un significativo aumento delle prestazioni.

Spesso puoi sfruttare la percezione umana imperfetta e visualizzare lo sfondo solo una volta o a una velocità inferiore rispetto a quello in primo piano (che probabilmente occupa 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 dello sfondo solo a ogni singolo frame. Tieni inoltre presente che questo approccio si applica bene a qualsiasi numero di canvas compositi se l'applicazione funziona meglio con questo tipo di struttura.

Evita shadowBlur

Come molti altri ambienti grafici, la tela HTML5 consente agli sviluppatori di sfocare 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);

Scopri i vari modi per cancellare i contenuti

Poiché il canvas HTML5 è un paradigma di disegno in modalità immediata, la scena deve essere ridisegnata esplicitamente in ogni frame. Per questo motivo, cancellare la tela è un'operazione fondamentale per le app e i giochi HTML5 di canvas. Come menzionato nella sezione Evita modifiche allo stato del canvas, spesso la cancellazione dell'intero canvas è indesiderabile, ma se è necessario ci sono due opzioni: chiamare context.clearRect(0, 0, width, height) o utilizzare un trucco specifico per le canvas: canvas.width = canvas.width. Al momento della scrittura, clearRect generalmente supera la versione di reimpostazione della larghezza, ma in alcuni casi l'utilizzo della compromissione con canvas.width è molto più veloce in Chrome 14.

Fai attenzione a questo suggerimento, poiché dipende in gran parte dall'implementazione di canvas di base ed è soggetto a molti cambiamenti. Per ulteriori informazioni, leggi l'articolo di Simon Sarris su come pulire la tela.

Evita le coordinate in virgola mobile

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

Pixel secondario

Se lo sprite levigato non è l'effetto che cerchi, potrebbe essere molto più veloce convertire le tue 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, le più efficaci delle quali prevedono l'aggiunta di una metà al numero target e l'esecuzione di 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;

L'analisi completa del rendimento è disponibile qui (jsperf).

Tieni presente che questo tipo di ottimizzazione non dovrebbe più avere importanza una volta che le implementazioni canvas hanno accelerazione GPU, in modo da poter eseguire rapidamente il rendering delle coordinate non intere.

Ottimizza le animazioni con requestAnimationFrame

L'API requestAnimationFrame relativamente nuova è il modo consigliato per implementare applicazioni interattive nel browser. Invece di comandare al browser di eseguire il rendering con una determinata frequenza di selezione fissa, chiedi educatamente al browser di chiamare la tua routine di rendering e di farti chiamare quando il browser è disponibile. Un buon effetto collaterale: se la pagina non è in primo piano, il browser è abbastanza intelligente da non rendersi più visibile. Il callback requestAnimationFrame punta a una frequenza di callback di 60 f/s, ma non lo garantisce, quindi devi tenere traccia del tempo trascorso dall'ultimo rendering. Potrebbe 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 uso di requestAnimationFrame si applica alla tela e ad altre tecnologie di rendering come WebGL. Al momento, questa API è disponibile solo in Chrome, Safari e Firefox, quindi devi utilizzare questo shim.

La maggior parte delle implementazioni di canvas mobile è lenta

Parliamo dei dispositivi mobili. Sfortunatamente, al momento della scrittura, solo per iOS 5.0 in versione beta con Safari 5.1 è disponibile l'implementazione del canvas mobile con accelerazione GPU. Senza l'accelerazione della GPU, i browser mobile in genere non dispongono di CPU abbastanza potenti per le moderne applicazioni basate su canvas. Alcuni dei test JSPerf descritti sopra mostrano risultati in ordine di grandezza peggiore sui dispositivi mobili rispetto ai test desktop, limitando notevolmente i tipi di app cross-device che è possibile prevedere di eseguire correttamente.

Conclusione

Per ricapitolare, questo articolo riguardava un set completo di utili tecniche di ottimizzazione che ti aiuteranno a sviluppare progetti basati su canvas HTML5 ad alte prestazioni. Ora che hai imparato qualcosa di nuovo, prosegui e ottimizza le tue fantastiche creazioni. In alternativa, se al momento non hai un gioco o un'applicazione da ottimizzare, dai un'occhiata agli esperimenti di Chrome e a Creative JS per trovare l'ispirazione.

Riferimenti