Beschleunigen von WebGL / Three.js mit OffscreenCanvas und Web Workers

Beschleunigen von WebGL / Three.js mit OffscreenCanvas und Web Workers

In diesem Tutorial werde ich Ihnen OffscreenCanvas , wie ich mit OffscreenCanvas den gesamten Code für die Arbeit mit WebGL und Three.js in einen separaten Webworker-Thread eingefügt habe . Dies beschleunigte die Arbeit der Site und auf schwachen Geräten verschwanden Friese beim Laden der Seite.

Der Artikel basiert auf persönlichen Erfahrungen, als ich meiner Website rotierende 3D-Erde hinzufügte und in Google Lighthouse 5 Produktivitätspunkte benötigte - zu viel für einfache Vorführungen.

Das Problem


Three.js verbirgt viele komplexe WebGL-Probleme, hat jedoch einen erheblichen Preis: Die Bibliothek fügt Ihrem JS-Build für Browser 563 KB hinzu (und die Bibliotheksarchitektur ermöglicht kein effizientes Trichashing).

Einige mögen sagen, dass Bilder oft die gleichen 500 KB wiegen - und sehr falsch sind. Jede KB des Skripts ist wesentlich leistungsfähiger als die KB des Bildes. Damit eine Site schnell ist, müssen Sie nicht nur die Kanalbreite und die Verzögerungszeit berücksichtigen, sondern auch die Betriebszeit der CPU des Computers für die Verarbeitung von Dateien. Auf Telefonen und schwachen Laptops kann die Verarbeitung länger dauern als das Laden.

Die 170K JS-Verarbeitung dauert 3,5 Sekunden gegenüber 0,1 Sekunden für 170K-Bilder
Die 170K JS-Verarbeitung dauert 3,5 Sekunden gegenüber 0,1 Sekunden für 170K-Bilder - Eddie Osmani

Während der Browser 500 KB Three.js ausführt, wird der Hauptseitenfluss blockiert und der Benutzer sieht den Schnittstellenfries.

Web Worker und Offscreen Canvas


Wir haben seit langem die Lösung gefunden, den Fries während der langen Laufzeit von JS-Web-Workern, die den Code in einem separaten Thread ausführen, nicht zu entfernen.

Damit die Arbeit mit Web-Workern nicht zur Hölle der Multithread-Programmierung wird, hat ein Web-Worker keinen Zugriff auf das DOM. Nur der Hauptthread funktioniert mit der HTML-Seite. Aber wie kann man Three.js ohne Zugriff auf das DOM starten, was direkten Zugriff auf <canvas> erfordert?

Zu diesem Zweck gibt es OffscreenCanvas , mit dem Sie <canvas> an einen Web-Worker übergeben können. Um die Tore der Multithread-Hölle nicht zu öffnen, verliert der Haupt-Thread nach der Übertragung den Zugriff auf diese <canvas> - nur ein Thread wird damit arbeiten.

Es scheint, dass wir dem Ziel nahe sind, aber es stellt sich heraus, dass nur Chrome OffscreenCanvas unterstützt.

Nur Chrome unterstützt OffscreenCanvas
OffscreenCanvas-Unterstützung für April 2019 gemäß Can I Use

Aber auch hier sollten wir angesichts des Hauptfeindes des Webentwicklers, der Browserunterstützung, nicht aufgeben. Wir kommen zusammen und finden das letzte Element des Puzzles - dies ist ein idealer Fall für "progressive Verbesserung". In Chrome und zukünftigen Browsern entfernen wir den Fries und andere Browser funktionieren wie zuvor.

Daher müssen wir eine Datei schreiben, die in zwei verschiedenen Umgebungen gleichzeitig arbeiten kann - in einem Web-Worker und in einem regulären Haupt-JS-Stream.

Lösung


Um die Hacks unter einer Zuckerschicht zu verstecken, habe ich eine kleine Offscreen-Canvas- JS-Bibliothek mit 400 Bytes (!) Erstellt . In den Beispielen wird der Code es verwenden, aber ich werde Ihnen sagen, wie es "unter der Haube" funktioniert.

Beginnen wir mit der Installation der Bibliothek:

 npm install offscreen-canvas 

Wir benötigen eine separate JS-Datei für den Web-Worker. Erstellen Sie eine separate Assembly-Datei in Webpack oder Parcel:

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

Die Kollektoren ändern den Dateinamen während der Bereitstellung aufgrund von Cache-Bustern ständig. Wir müssen den Namen mithilfe des Preload-Tags in HTML schreiben. Hier ist das Beispiel abstrakt, da der tatsächliche Code stark von den Funktionen Ihrer Assembly abhängt.

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

Jetzt müssen wir den DOM-Knoten für die <canvas> und den Inhalt des Preload-Tags in der Haupt-JS-Datei abrufen.

 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 wenn createWorker vorhanden ist, canvas.transferControlToOffscreen die JS-Datei in den Web Worker. Und in Abwesenheit dieser Methode - wie ein reguläres <script> .

Erstellen Sie diese webgl-worker.js für den Arbeiter:

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

insideWorker prüft, ob es in einen Web Worker geladen wurde. Abhängig von der Umgebung werden verschiedene Kommunikationssysteme mit dem Hauptthread gestartet.

Die Bibliothek führt für jede neue Nachricht vom Hauptthread eine an insideWorker Funktion aus. Unmittelbar nach dem Laden createWorker die erste Nachricht { canvas, width, height } createWorker { canvas, width, height } um den ersten Frame auf <canvas> zu zeichnen.

 + 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() } }) 

Wenn Sie Ihren alten Code für Three.js auf einen Web-Worker portieren, werden möglicherweise Fehler angezeigt, da der Web-Worker keine DOM-API hat. Beispielsweise gibt es kein document.createElement zum Laden von SVG-Texturen. Daher benötigen wir manchmal verschiedene Lader in einem Web-Worker und in einem regulären Skript. Um die Art der Umgebung zu überprüfen, haben wir 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() 

Wir haben den ersten Frame gezeichnet. Die meisten WebGL-Szenen sollten jedoch auf Benutzeraktionen reagieren. Drehen Sie beispielsweise die Kamera, wenn sich der Cursor bewegt, oder zeichnen Sie einen Rahmen, wenn die Fenstergröße geändert wird. Leider kann der Web-Worker keine DOM-Ereignisse abhören. Wir müssen sie im Hauptstrom anhören und Nachrichten an den Web-Worker senden.

  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() + } }) 

Ergebnis


Mit OffscreenCanvas ich Friese auf meiner Website besiegt und 100% Punkte bei Google Lighthouse erhalten. Und WebGL funktioniert in allen Browsern, auch ohne OffscreenCanvas Unterstützung.

Sie können sich die Live-Site und den Quellcode des Hauptthreads oder Workers ansehen.


Mit OffscreenCanvas stieg die Brille von Google Lighthouse von 95 auf 100

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


All Articles