
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 - Eddie OsmaniWä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.
OffscreenCanvas-Unterstützung für April 2019 gemäß Can I UseAber 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) {
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
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