Canvas性能優化

最近對 html5小遊戲有點興趣,由於我感受未來這個東西或許是前端一個重要的應用場景,例如如今每到某些節假日,像支付寶、淘寶或者其餘的一些 APP可能會給你推送通知,而後點進去就是一個小遊戲,基本上點進去的人,只要不是太抵觸,都會玩上一玩的,若是要是剛好 get到用戶的 G點,還能進一步加強業務,不管是用戶體驗,仍是對業務的發展,都是一種很不錯的提高方式。html

另外,我說的這個 html5小遊戲是包括 WebGLWebVR等在內的東西,不只限於遊戲,也能夠是其餘用到相關技術的場景,例如商品圖片 360°在線查看這種,之因此從小遊戲入手,是由於小遊戲須要的技術一應俱全,能把遊戲作好,再用相同的技術去作其餘的事情,就比較信手拈來了前端

查找資料,發現門道仍是蠻多的,看了一圈下來,決定從基礎入手,先從較爲簡單的 canvas 遊戲看起,看了一些相關文章和書籍,發現這個東西雖然用起來很簡單,可是真想用好,發揮其該有的能力仍是有點難度的,最好從實戰入手html5

因而最近準備寫個 canvas小遊戲練手,相關 UI素材已經蒐集好了,不過俗話說 工欲善其事必先利其器,因爲對這方面沒什麼經驗,因此爲了不過程當中出現的各類坑點,特意又看了一些相關的踩坑文章,其中性能我感受是必需要注意的地方,並且門道不少,因此整理了一下web

使用 requestNextAnimationFrame進行動畫循環

setTimeoutsetInterval並不是是專爲連續循環產生的 API,因此可能沒法達到流暢的動畫表現,故用 requestNextAnimationFrame,可能須要 polyfillcanvas

const raf = window.requestAnimationFrame
  || window.webkitRequestAnimationFrame
  || window.mozRequestAnimationFrame
  || window.oRequestAnimationFrame
  || window.msRequestAnimationFrame
  || function(callback) {
    window.setTimeout(callback, 1000 / 60)
  }
複製代碼

利用剪輯區域來處理動畫背景或其餘不變的圖像

若是隻是簡單動畫,那麼每一幀動畫擦除並重繪畫布上全部內容是可取的操做,但若是背景比較複雜,那麼可使用 剪輯區域技術,經過每幀較少的繪製來得到更好的性能api

利用剪輯區域技術來恢復上一幀動畫所佔背景圖的執行步驟:數組

  • 調用 context.save(),保存屏幕 canvas的狀態
  • 經過調用 beginPath來開始一段新的路徑
  • context對象上調用 arc()rect()等方法來設置路徑
  • 調用 context.clip()方法,將當前路徑設置爲屏幕 canvas的剪輯區域
  • 擦除屏幕 canvas中的圖像(實際上只會擦除剪輯區域所在的這一塊範圍)
  • 將背景圖像繪製到屏幕 canvas上(繪製操做實際上只會影響剪輯區域所在的範圍,因此每幀繪製圖像像素數更少)
  • 恢復屏幕 canvas的狀態參數,重置剪輯區域

離屏緩衝區(離屏canvas)

先繪製到一個離屏 canvas中,而後再經過 drawImage把離屏 canvas 畫到主 canvas中,就是把離屏 canvas當成一個緩存區。把須要重複繪製的畫面數據進行緩存起來,減小調用 canvasAPI的消耗瀏覽器

const cacheCanvas = document.createElement('canvas')
const cacheCtx = cacheCanvas.getContext('2d')
cacheCtx.width = 200
cacheCtx.height = 200
// 繪製到主canvas上
ctx.drawImage(0, 0)
複製代碼

雖然離屏 canvas在繪製以前視野內看不到,但其寬高最好設置得跟緩存元素的尺寸同樣,避免資源浪費,也避免繪製多餘的沒必要要圖像,同時在 drawImage時縮放圖像也將耗費資源 必要時,可使用多個離屏 canvas 另外,離屏 canvas再也不使用時,最好把手動將引用重置爲 null,避免由於 jsdom之間存在的關聯,致使垃圾回收機制沒法正常工做,佔用資源緩存

儘可能利用 CSS

背景圖

若是有大的靜態背景圖,直接繪製到 canvas可能並非一個很好的作法,若是能夠,將這個大背景圖做爲 background-image 放在一個 DOM元素上(例如,一個 div),而後將這個元素放到 canvas後面,這樣就少了一個 canvas的繪製渲染dom

transform變幻

CSStransform性能優於 canvastransfomr API,由於前者基於能夠很好地利用 GPU,因此若是能夠,transform變幻請使用 CSS來控制

關閉透明度

建立 canvas上下文的 API存在第二個參數:

canvas.getContext(contextType, contextAttributes)
複製代碼

contextType 是上下文類型,通常值都是 2d,除此以外還有 webglwebgl2bitmaprenderer三個值,只不事後面三個瀏覽器支持度過低,通常不用

contextAttributes 是上下文屬性,用於初始化上下文的一些屬性,對於不一樣的 contextTypecontextAttributes的可取值也不一樣,對於經常使用的 2dcontextAttributes可取值有:

  • alpha

boolean類型值,代表 canvas包含一個 alpha通道. 默認爲 true,若是設置爲 false, 瀏覽器將認爲 canvas背景老是不透明的, 這樣能夠加速繪製透明的內容和圖片

  • willReadFrequently

boolean類型值,代表是否有重複讀取計劃。常用 getImageData(),這將迫使軟件使用 2D canvas 並節省內存(而不是硬件加速)。這個方案適用於存在屬性 gfx.canvas.willReadFrequently的環境。並設置爲 true (缺省狀況下,只有B2G / Firefox OS)

支持度低,目前只有 Gecko內核的瀏覽器支持,不經常使用

  • storage

string 這樣表示使用哪一種方式存儲(默認爲:持久(persistent))

支持度低,目前只有 Blink內核的瀏覽器支持,不經常使用

上面三個屬性,看經常使用的 alpha就好了,若是你的遊戲使用畫布並且不須要透明,當使用 HTMLCanvasElement.getContext() 建立一個繪圖上下文時把alpha 選項設置爲 false ,這個選項能夠幫助瀏覽器進行內部優化

const ctx = canvas.getContext('2d', { alpha: false })
複製代碼

儘可能不要頻繁地調用比較耗時的API

例如

shadow相關 API,此類 API包括 shadowOffsetXshadowOffsetYshadowBlurshadowColor

繪圖相關的 API,例如 drawImageputImageData,在繪製時進行縮放操做也會增長耗時時間

固然,上述都是儘可能避免 頻繁調用,或用其餘手段來控制性能,須要用到的地方確定仍是要用的

避免浮點數的座標

利用 canvas進行動畫繪製時,若是計算出來的座標是浮點數,那麼可能會出現 CSS Sub-pixel的問題,也就是會自動將浮點數值四捨五入轉爲整數,那麼在動畫的過程當中,因爲元素實際運動的軌跡並非嚴格按照計算公式獲得,那麼就可能出現抖動的狀況,同時也可能讓元素的邊緣出現抗鋸齒失真 這也是可能影響性能的一方面,由於一直在作沒必要要的取證運算

渲染繪製操做不要頻繁調用

渲染繪製的 api,例如 stroke()filldrawImage,都是將 ctx狀態機裏面的狀態真實繪製到畫布上,這種操做也比較耗費性能

例如,若是你要繪製十條線段,那麼先在 ctx狀態機中繪製出十天線段的狀態機,再進行一次性的繪製,這將比每條線段都繪製一次要高效得多

for (let i = 0; i < 10; i++) {
  context.beginPath()
  context.moveTo(x1[i], y1[i])
  context.lineTo(x2[i], y2[i])
  // 每條線段都單獨調用繪製操做,比較耗費性能
  context.stroke()
}

for (let i = 0; i < 10; i++) {
  context.beginPath()
  context.moveTo(x1[i], y1[i])
  context.lineTo(x2[i], y2[i])
}
// 先繪製一條包含多條線條的路徑,最後再一次性繪製,能夠獲得更好的性能
context.stroke()
複製代碼

儘可能少的改變狀態機 ctx的裏狀態

ctx能夠看作是一個狀態機,例如 fillStyleglobalAlphabeginPath,這些 api都會改變 ctx裏面對於的狀態,頻繁改變狀態機的狀態,是影響性能的

能夠經過對操做進行更好的規劃,減小狀態機的改變,從而獲得更加的性能,例如在一個畫布上繪製幾行文字,最上面和最下面文字的字體都是 30px,顏色都是 yellowgreen,中間文字是 20px pink,那麼能夠先繪製最上面和最下面的文字,再繪製中間的文字,而非必須從上往下依次繪製,由於前者減小了一次狀態機的狀態改變

const c = document.getElementById("myCanvas")
const ctx = c.getContext("2d")

ctx.font = '30 sans-serif'
ctx.fillStyle = 'yellowgreen'
ctx.fillText("你們好,我是最上面一行", 0, 40)

ctx.font = '20 sans-serif'
ctx.fillStyle = 'red'
ctx.fillText("你們好,我是中間一行", 0, 80)

ctx.font = '30 sans-serif'
ctx.fillStyle = 'yellowgreen'
ctx.fillText("你們好,我是最下面一行", 0, 130)
複製代碼

下面的代碼實現的效果和上面相同,可是代碼量更少,同時比上述代碼少改變了一次狀態機,性能會更好

ctx.font = '30 sans-serif'
ctx.fillStyle = 'yellowgreen'
ctx.fillText("你們好,我是最上面一行", 0, 40)
ctx.fillText("你們好,我是最下面一行", 0, 130)

ctx.font = '20 sans-serif'
ctx.fillStyle = 'red'
ctx.fillText("你們好,我是中間一行", 0, 80)
複製代碼

儘可能少的調用 canvas API

嗯,canvas也是經過操縱 js來繪製的,可是相比於正常的 js操做,調用 canvas API將更加消耗資源,因此在繪製以前請作好規劃,經過 適量 js原生計算減小 canvas API的調用是一件比較划算的事情

固然,請注意 適量二字,若是減小一行 canvas API調用的代價是增長十行 js計算,那這事可能就不必作了

避免阻塞

在進行某些耗時操做,例如計算大量數據,一幀中包含了太多的繪製狀態,大規模的 DOM操做等,可能會致使頁面卡頓,影響用戶體驗,能夠經過如下兩種手段:

web worker

web worker最經常使用的場景就是大量的頻繁計算,減輕主線程壓力,若是遇到大規模的計算,能夠經過此 API分擔主線程壓力,此 API兼容性已經很不錯了,既然 canvas能夠用,那 web worker也就徹底能夠考慮使用

分解任務

將一段大的任務過程分解成數個小型任務,使用定時器輪詢進行,想要對一段任務進行分解操做,此任務須要知足如下狀況:

  • 循環處理操做並不要求同步
  • 數據並不要求按照順序處理

分解任務包括兩種情形:

  • 根據任務總量分配

例如進行一個千萬級別的運算總任務,能夠將其分解爲 10個百萬級別的運算小任務

// 封裝 定時器分解任務 函數
function processArray(items, process, callback) {
  // 複製一份數組副本
  var todo=items.concat();
  setTimeout(function(){
    process(todo.shift());
    if(todo.length>0) {
      // 將當前正在執行的函數自己再次使用定時器
      setTimeout(arguments.callee, 25);
    } else {
      callback(items);
    }
  }, 25);
}

// 使用
var items=[12,34,65,2,4,76,235,24,9,90];
function outputValue(value) {
  console.log(value);
}
processArray(items, outputValue, function(){
  console.log('Done!');
});
複製代碼

優勢是任務分配模式比較簡單,更有控制權,缺點是很差肯定小任務的大小

有的小任務可能由於某些緣由,會耗費比其餘小任務更多的時間,這會形成線程阻塞;而有的小任務可能須要比其餘任務少得多的時間,形成資源浪費

  • 根據運行時間分配

例如運行一個千萬級別的運算總任務,不直接肯定分配爲多少個子任務,或者分配的顆粒度比較小,在每個或幾個計算完成後,查看此段運算消耗的時間,若是時間小於某個臨界值,好比 10ms,那麼就繼續進行運算,不然就暫停,等到下一個輪詢再進行進行

function timedProcessArray(items, process, callback) {
  var todo=items.concat();
  setTimeout(function(){
    // 開始計時
    var start = +new Date();
    // 若是單個數據處理時間小於 50ms ,則無需分解任務
    do {
      process(todo.shift());
    } while (todo.length && (+new Date()-start < 50));

    if(todo.length > 0) {
      setTimeout(arguments.callee, 25);
    } else {
      callback(items);
    }
  });
}
複製代碼

優勢是避免了第一種狀況出現的問題,缺點是多出了一個時間比較的運算,額外的運算過程也可能影響到性能

總結

我準備作的 canvas遊戲彷佛須要的製做時間有點長,天天除了上班以外,剩下的時間實在是很少,不知道何時能搞完,若是一切順利,我卻是還想再用一些遊戲引擎,例如 EgretLayaAirCocos Creator 將其重製一遍,以熟悉這些遊戲引擎的用法,而後到時候寫個系列教程出來……

誒,這麼看來,彷佛是要持久戰了啊

相關文章
相關標籤/搜索