Acelerar WebGL / Three.js con OffscreenCanvas y Web Workers

Acelerar WebGL / Three.js con OffscreenCanvas y Web Workers

En este tutorial, le OffscreenCanvas cómo usar OffscreenCanvas para obtener todo el código para trabajar con WebGL y Three.js en un hilo de trabajo web separado. Esto aceleró el trabajo del sitio y en dispositivos débiles los frisos desaparecieron durante la carga de la página.

El artículo se basa en la experiencia personal, cuando agregué tierra 3D giratoria a mi sitio web y se necesitaron 5 puntos de productividad en Google Lighthouse , demasiado para presumir fácilmente.

El problema


Three.js oculta muchos problemas complejos de WebGL, pero tiene un precio muy alto: la biblioteca agrega 563 KB a su compilación JS para los navegadores (y la arquitectura de la biblioteca no permite que el trichashing funcione de manera eficiente).

Algunos pueden decir que las imágenes a menudo pesan los mismos 500 KB, y estarán muy equivocadas. Cada KB del script tiene un rendimiento mucho más potente que el KB de la imagen. Para que un sitio sea rápido, debe pensar no solo en el ancho del canal y el tiempo de retraso, sino también en el tiempo de funcionamiento de la CPU de la computadora para procesar archivos. En teléfonos y computadoras portátiles débiles, el procesamiento puede llevar más tiempo que la carga.

El procesamiento de 170K JS tarda 3,5 segundos frente a 0,1 segundos para una imagen de 170K
El procesamiento de 170K JS tarda 3.5 segundos versus 0.1 segundo para imágenes de 170K - Eddie Osmani

Mientras el navegador ejecutará 500 KB Three.js, el flujo de la página principal se bloqueará y el usuario verá el friso de la interfaz.

Trabajadores web y lienzo fuera de pantalla


Durante mucho tiempo hemos tenido una solución para no eliminar el friso durante el largo plazo de JS: los trabajadores web que ejecutan el código en un hilo separado.

Para que trabajar con trabajadores web no se convierta en un infierno de programación multiproceso, un trabajador web no tiene acceso al DOM. Solo el hilo principal funciona con la página HTML. Pero, ¿cómo iniciar Three.js sin acceso al DOM, que requiere acceso directo a <canvas> ?

Para hacer esto, hay OffscreenCanvas : le permite pasar <canvas> a un trabajador web. Para no abrir las puertas del infierno multiproceso, después de la transferencia, el hilo principal pierde acceso a este <canvas> , solo un hilo funcionará con él.

Parece que estamos cerca del objetivo, pero resulta que solo Chrome admite OffscreenCanvas .

Solo Chrome admite OffscreenCanvas
Compatibilidad con OffscreenCanvas para abril de 2019 según Can I Use

Pero incluso aquí, frente al principal enemigo del desarrollador web, el soporte del navegador, no debemos rendirnos. Nos reunimos y encontramos el último elemento del rompecabezas: este es un caso ideal para la "mejora progresiva". En Chrome y en los futuros navegadores, eliminaremos el friso y otros navegadores funcionarán como antes.

Como resultado, tendremos que escribir un archivo que pueda funcionar en dos entornos diferentes a la vez: en un trabajador web y en un flujo JS principal regular.

Solución


Para ocultar los hacks bajo una capa de azúcar, hice una pequeña biblioteca JS fuera del lienzo de 400 bytes (!). En los ejemplos, el código lo usará, pero le diré cómo funciona "bajo el capó".

Comencemos instalando la biblioteca:

 npm install offscreen-canvas 

Necesitamos un archivo JS separado para el trabajador web: cree un archivo de ensamblaje separado en Webpack o Parcel:

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

Los recopiladores cambiarán constantemente el nombre del archivo durante la implementación debido a los buscadores de caché; necesitaremos escribir el nombre en HTML utilizando la etiqueta de precarga . Aquí el ejemplo será abstracto, ya que el código real dependerá en gran medida de las características de su ensamblaje.

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

Ahora necesitamos obtener el nodo DOM para <canvas> y el contenido de la etiqueta de precarga en el archivo 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 si hay canvas.transferControlToOffscreen cargará el archivo JS en el trabajador web. Y en ausencia de este método, como un <script> normal.

Cree este webgl-worker.js para el trabajador:

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

insideWorker comprueba si se ha cargado dentro de un trabajador web. Dependiendo del entorno, lanzará diferentes sistemas de comunicación con el hilo principal.

La biblioteca ejecutará una función pasada a insideWorker para cada nuevo mensaje del hilo principal. Inmediatamente después de cargar, createWorker enviará el primer mensaje { canvas, width, height } para dibujar el primer marco en <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() } }) 

Al portar su antiguo código para Three.js a un trabajador web, puede ver errores, ya que el trabajador web no tiene una API DOM. Por ejemplo, no hay document.createElement para cargar texturas SVG. Entonces, a veces necesitaremos diferentes cargadores en un trabajador web y dentro de un script regular. Para verificar el tipo de entorno, tenemos 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() 

Dibujamos el primer cuadro. Pero la mayoría de las escenas de WebGL deberían responder a las acciones del usuario. Por ejemplo, gire la cámara cuando el cursor se mueva o dibuje un marco cuando la ventana cambie de tamaño. Desafortunadamente, el trabajador web no puede escuchar eventos DOM. Necesitamos escucharlos en la transmisión principal y enviar mensajes al trabajador 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() + } }) 

Resultado


Con OffscreenCanvas derroté a los frisos en mi sitio y obtuve 100% de puntos en Google Lighthouse. Y WebGL funciona en todos los navegadores, incluso sin el soporte OffscreenCanvas .

Puede echar un vistazo al sitio en vivo y al código fuente del hilo principal o trabajador .


Con OffscreenCanvas, las gafas Google Lighthouse aumentaron de 95 a 100

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


All Articles