
Dans ce tutoriel, je vais vous
OffscreenCanvas
comment, en utilisant
OffscreenCanvas
j'ai réussi à mettre tout le code pour travailler avec WebGL et
Three.js dans un thread de travail Web distinct. Cela a accéléré le travail du site et sur les appareils faibles, les frises ont disparu lors du chargement de la page.
L'article est basé sur l'expérience personnelle, lorsque j'ai
ajoutĂ© la rotation de la terre 3D Ă
mon site Web et qu'il a fallu 5 points de productivité dans
Google Lighthouse - trop pour des démonstrations faciles.
Le problème
Three.js cache un grand nombre de problèmes WebGL complexes, mais il a un prix sérieux - la bibliothèque ajoute
563 Ko à votre build JS pour les navigateurs (et l'architecture de la bibliothèque ne permet pas au trichashing de fonctionner efficacement).
Certains diront que les images pèsent souvent les mêmes 500 Ko - et seront très fausses. Chaque Ko du script est beaucoup plus performant que les Ko de l'image. Pour qu'un site soit rapide, vous devez penser non seulement à la largeur du canal et au temps de retard - vous devez également penser à la durée de fonctionnement du processeur de l'ordinateur pour le traitement des fichiers. Sur les téléphones et les ordinateurs portables faibles, le traitement peut prendre plus de temps que le chargement.
Le traitement JS 170K prend 3,5 secondes contre 0,1 seconde pour les images 170K - Eddie OsmaniAlors que le navigateur exécutera 500 Ko Three.js, le flux de la page principale sera bloqué et l'utilisateur verra la frise d'interface.
Travailleurs Web et toile hors écran
Nous avons depuis longtemps une solution pour ne pas supprimer la frise pendant la longue période de JS - les travailleurs Web exécutant le code dans un thread séparé.
Pour que travailler avec des travailleurs Web ne se transforme pas en enfer de programmation multithread, un travailleur Web n'a pas accès au DOM. Seul le thread principal fonctionne avec la page HTML. Mais comment dĂ©marrer Three.js sans accès au DOM, ce qui nĂ©cessite un accès direct Ă
<canvas>
?
Pour ce faire, il existe
OffscreenCanvas - il vous permet de passer
<canvas>
à un travailleur Web. Afin de ne pas ouvrir les portes de l'enfer multithread, après le transfert, le thread principal perd l'accès à ce
<canvas>
- un seul thread fonctionnera avec lui.
Il semble que nous soyons proches de l'objectif, mais il s'avère que seul Chrome prend en charge
OffscreenCanvas
.
Prise en charge de la toile hors écran pour avril 2019 selon Can I UseMais même ici, face à l'ennemi principal du développeur Web, le support du navigateur, nous ne devons pas abandonner. Nous nous réunissons et trouvons le dernier élément du puzzle - c'est un cas idéal pour «l'amélioration progressive». Dans Chrome et les futurs navigateurs, nous supprimerons la frise et les autres navigateurs fonctionneront comme auparavant.
Par conséquent, nous devrons écrire un fichier qui peut fonctionner dans deux environnements différents à la fois - dans un travailleur Web et dans un flux JS principal régulier.
Solution
Pour cacher les hacks sous une couche de sucre, j'ai fait une petite bibliothèque JS
hors toile de 400 octets (!). Dans les exemples, le code l'utilisera, mais je vais vous expliquer comment cela fonctionne "sous le capot".
Commençons par installer la bibliothèque:
npm install offscreen-canvas
Nous avons besoin d'un fichier JS distinct pour le travailleur Web - créez un fichier d'assemblage séparé dans Webpack ou Parcel:
entry: { 'app': './src/app.js', + 'webgl-worker': './src/webgl-worker.js' }
Les collecteurs changeront constamment le nom du fichier pendant le déploiement à cause des cache-cache - nous devrons écrire le nom en HTML à l'aide de la
balise de préchargement . Ici, l'exemple sera abstrait, car le code réel dépendra grandement des fonctionnalités de votre assembly.
<link type="preload" as="script" href="./webgl-worker.js"> </head>
Maintenant, nous devons obtenir le nœud DOM pour le
<canvas>
et le contenu de la balise de préchargement dans le fichier JS principal.
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
s'il y a
canvas.transferControlToOffscreen
chargera le fichier JS dans le Web Worker. Et en l'absence de cette méthode - comme un
<script>
ordinaire.
Créez ce
webgl-worker.js
pour le travailleur:
import insideWorker from 'offscreen-canvas/inside-worker' const worker = insideWorker(e => { if (e.data.canvas) {
insideWorker
vérifie s'il a été chargé dans un travailleur Web. Selon l'environnement, il lancera différents systèmes de communication avec le fil principal.
La bibliothèque exĂ©cutera une fonction transmise Ă
insideWorker
pour chaque nouveau message du thread principal. Immédiatement après le chargement,
createWorker
enverra le premier message
{ canvas, width, height }
pour dessiner le premier cadre sur
<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() } })
Lors du portage de votre ancien code pour Three.js vers un travailleur Web, vous pouvez voir des erreurs, car il n'y a pas d'API DOM dans le travailleur Web. Par exemple, il n'y a pas de
document.createElement
pour charger les textures SVG. Donc, parfois, nous aurons besoin de différents chargeurs dans un travailleur Web et dans un script normal. Pour vérifier le type d'environnement, nous avons
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()
Nous avons dessiné le premier cadre. Mais la plupart des scènes WebGL doivent répondre aux actions des utilisateurs. Par exemple, faites pivoter la caméra lorsque le curseur se déplace ou dessinez un cadre lorsque la fenêtre est redimensionnée. Malheureusement, le travailleur Web ne peut pas écouter les événements DOM. Nous devons les écouter dans le flux principal et envoyer des messages au travailleur Web.
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
Résultat
Avec
OffscreenCanvas
j'ai vaincu des frises sur mon site et obtenu 100% de points sur Google Lighthouse. Et WebGL fonctionne dans tous les navigateurs, mĂŞme sans le support
OffscreenCanvas
.
Vous pouvez jeter un œil au
site en direct et au
code source du thread principal ou du
travailleur .
Avec OffscreenCanvas, les lunettes Google Lighthouse sont passées de 95 à 100