OffscreenCanvasとWeb WorkersによるWebGL / Three.jsの高速化

OffscreenCanvasとWeb WorkersでWebGL / Three.jsを高速化

このチュートリアルでは、 OffscreenCanvasを使用して、WebGLとThree.jsを操作するためのすべてのコードを別のWebワーカースレッドに配置する方法をOffscreenCanvasます。 これによりサイトの作業が加速し、脆弱なデバイスではページの読み込み中にフリーズが消えました。

この記事は個人的な経験に基づいており、3D地球の回転を自分のサイト追加したとき、 Google Lighthouseで5ポイントの生産性が必要でした。

問題


Three.jsは、WebGLの複雑な問題の多くを隠しますが、深刻な代償を伴います-ライブラリは、ブラウザのJSアセンブリに563 KBを追加します(ライブラリアーキテクチャでは、トライシャッシングが効果的に機能しません)。

写真の重量は同じ500 KBであることが多いと言う人もいますが、これは非常に間違っています。 スクリプトの各KBは、イメージのKBよりもパフォーマンスがはるかに強力です。 サイトを高速化するには、チャネル幅と遅延時間だけでなく、ファイルを処理するためのコンピューターのCPUの動作時間も考慮する必要があります。 携帯電話や弱いノートパソコンでは、処理に時間がかかる場合があります。

170K JS処理には3.5秒かかりますが、170K画像の場合は0.1秒です。
170K JS処理には3.5秒かかりますが、170K画像の場合は0.1秒です-Eddie Osmani

ブラウザーは500 KB Three.jsを実行しますが、メインページフローはブロックされ、ユーザーにはインターフェイスフリーズが表示されます。

Web Workersとオフスクリーンキャンバス


私たちは長い間、JSの実行中にフリーズを削除しないという解決策を持っていました-Webワーカーは別のスレッドでコードを実行しています。

そのため、Webワーカーでの作業がマルチスレッドプログラミングの地獄にならないように、WebワーカーはDOMにアクセスできません。 メインスレッドのみがHTMLページで動作します。 しかし、 <canvas>への直接アクセスが必要なDOMにアクセスせずにThree.jsを起動する方法は?

これを行うには、 OffscreenCanvasがあります - <canvas>をWebワーカーに渡すことができます。 マルチスレッドの地獄の門を開かないために、転送後、メインスレッドはこの<canvas>へのアクセスを失います<canvas>つのスレッドのみがそれで動作します。

目標に近づいているようですが、ChromeだけがOffscreenCanvasサポートしていOffscreenCanvas

ChromeのみがOffscreenCanvasをサポートしています
Can I Useによる 2019年4月のOffscreenCanvasサポート

しかし、ここでも、Web開発者の主な敵であるブラウザサポートに直面しても、giveめるべきではありません。 私たちは集まってパズルの最後の要素を見つけます-これは「進歩的な改善」の理想的なケースです。 Chromeおよび将来のブラウザでは、フリーズを削除し、他のブラウザは以前と同様に機能します。

そのため、2つの異なる環境(Webワーカーと通常のメインJSストリーム)で同時に機能する1つのファイルを作成する必要があります。

解決策


砂糖の層の下にハッキングを隠すために、400バイト(!)の小さなオフスクリーンキャンバス JSライブラリを作成しました。 例では、コードがそれを使用しますが、「内部で」どのように機能するかを説明します。

ライブラリをインストールすることから始めましょう:

 npm install offscreen-canvas 

Web Workerには個別のJSファイルが必要です-WebpackまたはParcelで個別のアセンブリファイルを作成します。

  entry: { 'app': './src/app.js', + 'webgl-worker': './src/webgl-worker.js' } 

コレクターは、キャッシュバスターのために、展開中にファイル名を絶えず変更します。 プリロードタグを使用してHTMLで名前を記述する必要があります。 実際のコードはアセンブリの機能に大きく依存するため、この例は抽象的です。

  <link type="preload" as="script" href="./webgl-worker.js"> </head> 

ここで、 <canvas>のDOMノードと、メインJSファイルのプリロードタグのコンテンツを取得する必要があります。

 import createWorker from 'offscreen-canvas/create-worker' const workerUrl = document.querySelector('[rel=preload][as=script]').href const canvas = document.querySelector('canvas') const worker = createWorker(canvas, workerUrl) 

createWorker場合、 canvas.transferControlToOffscreen JSファイルをWebワーカーにアップロードします。 そして、このメソッドがない場合-通常の<script>

ワーカー用にこのwebgl-worker.jsを作成します。

 import insideWorker from 'offscreen-canvas/inside-worker' const worker = insideWorker(e => { if (e.data.canvas) { //       <canvas> } }) 

insideWorkerは、Web Worker内にロードされているかどうかを確認します。 環境に応じて、メインスレッドと異なる通信システムを起動します。

ライブラリは、メインスレッドからの新しいメッセージごとにinsideWorkerに渡される関数を実行します。 ロード直後に、 createWorkerは最初のメッセージ{ canvas, width, height }を送信して、 <canvas>最初のフレームを描画し<canvas>

 + import { + WebGLRenderer, Scene, PerspectiveCamera, AmbientLight, + Mesh, SphereGeometry, MeshPhongMaterial + } from 'three' import insideWorker from 'offscreen-canvas/inside-worker' + const scene = new Scene() + const camera = new PerspectiveCamera(45, 1, 0.01, 1000) + scene.add(new AmbientLight(0x909090)) + + let sphere = new Mesh( + new SphereGeometry(0.5, 64, 64), + new MeshPhongMaterial() + ) + scene.add(sphere) + + let renderer + function render () { + renderer.render(scene, camera) + } const worker = insideWorker(e => { if (e.data.canvas) { + // canvas  -    —    ,     Three.js + if (!canvas.style) canvas.style = { width, height } + renderer = new WebGLRenderer({ canvas, antialias: true }) + renderer.setPixelRatio(pixelRatio) + renderer.setSize(width, height) + + render() } }) 

Three.jsの古いコードをWebワーカーに転送すると、WebワーカーにDOM APIがないため、エラーが表示される場合があります。 たとえば、ロードおよびSVGテクスチャ用のdocument.createElementはありません。 そのため、Webワーカーと通常のスクリプト内で異なるローダーが必要になる場合があります。 環境のタイプを確認するために、 worker.isWorkerがありworker.isWorker

  renderer.setPixelRatio(pixelRatio) renderer.setSize(width, height) + const loader = worker.isWorker ? new ImageBitmapLoader() : new ImageLoader() + loader.load('/texture.png', mapImage => { + sphere.material.map = new CanvasTexture(mapImage) + render() + }) render() 

最初のフレームを描きました。 ただし、ほとんどのWebGLシーンはユーザーアクションに応答する必要があります。 たとえば、カーソルが移動したときにカメラを回転させたり、ウィンドウのサイズを変更したときにフレームを描画したりします。 残念ながら、WebワーカーはDOMイベントをリッスンできません。 メインストリームでそれらを聞いて、ウェブワーカーにメッセージを送信する必要があります。

  import createWorker from 'offscreen-canvas/create-worker' const workerUrl = document.querySelector('[rel=preload][as=script]').href const canvas = document.querySelector('canvas') const worker = createWorker(canvas, workerUrl) + window.addEventListener('resize', () => { + worker.post({ + type: 'resize', width: canvas.clientWidth, height: canvas.clientHeight + }) + }) 

  const worker = insideWorker(e => { if (e.data.canvas) { if (!canvas.style) canvas.style = { width, height } renderer = new WebGLRenderer({ canvas, antialias: true }) renderer.setPixelRatio(pixelRatio) renderer.setSize(width, height) const loader = worker.isWorker ? new ImageBitmapLoader() : new ImageLoader() loader.load('/texture.png', mapImage => { sphere.material.map = new CanvasTexture(mapImage) render() }) render() - } + } else if (e.data.type === 'resize') { + renderer.setSize(width, height) + render() + } }) 

結果


OffscreenCanvasして、サイトでフリーズを倒し、Google Lighthouseで100%ポイントを獲得しました。 また、 OffscreenCanvasサポートされていなくても、WebGLはすべてのブラウザーで動作します。

ライブサイトメインスレッドまたはワーカーの ソースコードを確認できます。


OffscreenCanvasを使用すると、Google Lighthouseメガネは95から100に上がりました

Source: https://habr.com/ru/post/J446682/


All Articles