離屏Canvas — 使用Web Worker提高你的Canvas運行速度
現在因為有了離屏Canvas,你可以不用在你的主線程中繪制圖像了!
Canvas 是一個非常受歡迎的表現方式,同時也是WebGL的入口。它能繪制圖形,圖片,展示動畫,甚至是處理視頻內容。它經常被用來在富媒體web應用中創建炫酷的用戶界面或者是制作在線(web)游戲。
它是非常靈活的,這意味着繪制在Canvas的內容可以被編程。舉個🌰,JavaScript就提供了Canvas的系列API。這些給了Canvas非常好的靈活度。
但同時,在一些現代化的web站點,腳本解析運行是實現流暢用戶反饋的最大的問題之一。因為Canvas計算和渲染和用戶操作響應都發生在同一個線程中,在動畫中(有時候很耗時)的計算操作將會導致App卡頓,降低用戶體驗。
幸運的是, OffscreenCanvas 離屏Canvas可以非常棒的解決這個麻煩!
到目前為止,Canvas的繪制功能都與<canvas>
標簽綁定在一起,這意味着Canvas API和DOM是耦合的。而OffscreenCanvas,正如它的名字一樣,通過將Canvas移出屏幕來解耦了DOM和Canvas API。
由於這種解耦,OffscreenCanvas的渲染與DOM完全分離了開來,並且比普通Canvas速度提升了一些,而這只是因為兩者(Canvas和DOM)之間沒有同步。但更重要的是,將兩者分離后,Canvas將可以在Web Worker中使用,即使在Web Worker中沒有DOM。這給Canvas提供了更多的可能性。
在Worker中使用OffscreenCanvas
Workers 是一個Web版的線程——它允許你在幕后運行你的代碼。將你的一部分代碼放到Worker中可以給你的主線程更多的空閑時間,這可以提高你的用戶體驗度。就像其沒有DOM一樣,直到現在,在Worker中都沒有Canvas API。
而OffscreenCanvas並不依賴DOM,所以在Worker中Canvas API可以被某種方法來代替。下面是我在Worker中用OffscreenCanvas來計算漸變顏色的🌰:
// file: worker.js function getGradientColor(percent) { const canvas = new OffscreenCanvas(100, 1); const ctx = canvas.getContext('2d'); const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); gradient.addColorStop(0, 'red'); gradient.addColorStop(1, 'blue'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, ctx.canvas.width, 1); const imgd = ctx.getImageData(0, 0, ctx.canvas.width, 1); const colors = imgd.data.slice(percent * 4, percent * 4 + 4); return rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, ${colors[]); } getGradientColor(40); // rgba(152, 0, 104, 255 )
不要阻塞主線程
當我們將大量的計算移到Worker中運行時,可以釋放主線程上的資源,這很有意思。我們可以使用transferControlToOffscreen 方法將常規的Canvas映射到OffscreenCanvas實例上。之后所有應用於OffscreenCanvas的操作將自動呈現在在源Canvas上。
const offscreen = document.querySelector('canvas').transferControlToOffscreen(); const worker = new Worker('myworkerurl.js'); worker.postMessage({ canvas: offscreen }, [offscreen]);
OffscreenCanvas 是 [可轉移的](https://developer.mozilla.org/en-US/docs/Web/API/Transferable))。除了將其指定為傳遞信息中的字段之一以外,還需要將其作為postMessage(傳遞信息給Worker的方法)中的第二個參數傳遞出去,以便可以在Worker線程的context(上下文)中使用它。
在下面的🌰中,當顏色主題發生變化時會發生“復雜的計算”,這個計算即使在高性能的台式機上也要花費幾毫秒。而你可以選擇在主線程或Worker上運行這段動畫。在主線程下,當復雜計算開始運行時,你將無法與按鈕交互 - 線程被阻塞掉了。而在Worker下,UI的響應並沒有被影響。
它也是另一種解釋方式:任務繁忙的主線程也不會影響在Worker上運行的動畫。所以即使主線程非常繁忙,你也可以通過此功能來避免掉幀並保證流暢的動畫:
上例展示了在普通Canvas的下,當主線程被添加繁忙任務時動畫被阻塞了,而基於Worker的OffscreenCanvas播放卻很流利。
與流行庫一起使用
得益於OffscreenCanvas API一般情況下與常規Canvas元素的相API兼容,你可以很輕松地漸進地使用它,也可以使用社區里的一些優秀的圖形處理的庫/框架。
舉個🌰,你可以對其進行特征檢測,如果可用的話,可通過在渲染的構造函數中指定canvas的配置項,然后實現與Three.js一起使用的功能:
const canvasEl = document.querySelector("canvas"); const canvas = ('OffscreenCanvas' in window) ? canvasEl.transferControlToOffscreen() : canvasEl; canvas.style = { width: 0, height: 0 } const renderer = new THREE.WebGLRenderer({ canvas: canvas });
上例的問題是Three.js需要Canvas具有style.width和style.height屬性。而OffscreenCanvas是與DOM完全分離的,沒有這些屬性。所以你需要自己提供這些屬性,或者通過將其從three.js邏輯中刪除或者自行編寫這些值與初始Canvas尺寸相關聯的邏輯。
下面是一個運行基本Three.js動畫的demo:
但是請記住,有一些與DOM相關的API在Worker中並不容易獲得,因此如果你想使用更高級的Three.js功能(比如紋理)的話,可能需要更多變通的方法。有關這方面已經開始嘗試的一些想法,請查看 Google I/O 2017的視頻。
此視頻的示例中出現的commit()方法我們並不推薦。請改用worker.requestAnimationFrame。
結論
如果你對圖像繪畫使用得非常多,OffscreenCanvas可以有效的提高你APP的性能。它使得Worker可以處理canvas的渲染繪制,讓你的APP更好地利用了多核系統。
OffscreenCanvas在Chrome 69中已經不需要開啟flag(實驗性功能)就可以使用了。它也正在被 Firefox 實現。由於其API與普通canvas元素非常相似,所以你可以輕松地對其進行特征檢測並循序漸進地使用它,而不會破壞現有的APP或庫的運行邏輯。OffscreenCanvas在任何涉及到圖形計算以及動畫表現且與DOM關系並不密切(即依賴DOM API不多)的情況下,它都具有性能優勢。