Accélérer WebGL / Three.js avec OffscreenCanvas et Web Workers

Accélérer WebGL / Three.js avec OffscreenCanvas et Web Workers

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 l'image 170K
Le traitement JS 170K prend 3,5 secondes contre 0,1 seconde pour les images 170K - Eddie Osmani

Alors 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 .

Seul Chrome prend en charge OffscreenCanvas
Prise en charge de la toile hors écran pour avril 2019 selon Can I Use

Mais 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) { //       <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 === 'resize') { + renderer.setSize(width, height) + render() + } }) 

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

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


All Articles