Acelerando o WebGL / Three.js com OffscreenCanvas e Web Workers

Acelerando o WebGL / Three.js com OffscreenCanvas e Web Workers

Neste tutorial, OffscreenCanvas como, usando o OffscreenCanvas consegui colocar todo o código para trabalhar com o WebGL e o Three.js em um thread de trabalho da Web separado. Isso acelerou o trabalho do site e, em dispositivos fracos, os frisos desapareceram durante o carregamento da página.

O artigo baseia-se na experiência pessoal, quando adicionei terra 3D em rotação ao meu site e foram necessários 5 pontos de produtividade no Google Lighthouse - demais para exibições fáceis.

O problema


O Three.js oculta muitos problemas complexos de WebGL, mas tem um preço sério - a biblioteca adiciona 563 KB à sua compilação JS para navegadores (e a arquitetura da biblioteca não permite que o trichashing funcione com eficiência).

Alguns podem dizer que as fotos costumam pesar os mesmos 500 KB - e estarão muito erradas. Cada KB do script tem desempenho muito mais poderoso que o KB da imagem. Para que um site seja rápido, você precisa pensar não apenas na largura do canal e no tempo de atraso - mas também no tempo de operação da CPU do computador para processar arquivos. Em telefones e laptops fracos, o processamento pode demorar mais do que carregar.

O processamento de 170K JS leva 3,5 segundos contra 0,1 segundo para a imagem de 170K
O processamento de 170K JS leva 3,5 segundos contra 0,1 segundo para imagens de 170K - Eddie Osmani

Enquanto o navegador executará 500 KB Three.js, o fluxo da página principal será bloqueado e o usuário verá o friso da interface.

Trabalhadores da Web e telas fora da tela


Há muito tempo temos uma solução para não remover o friso durante o longo prazo dos trabalhadores da JS - web executando o código em um encadeamento separado.

Para que trabalhar com trabalhadores da Web não se torne um inferno de programação multithread, um trabalhador da Web não tem acesso ao DOM. Somente o encadeamento principal funciona com a página HTML. Mas como iniciar o Three.js sem acesso ao DOM, o que requer acesso direto ao <canvas> ?

Para fazer isso, existe o OffscreenCanvas - ele permite que você passe <canvas> para um trabalhador da Web. Para não abrir os portões do inferno multithread, após a transferência, o thread principal perde acesso a este <canvas> - apenas um thread funcionará com ele.

Parece que estamos perto do objetivo, mas acontece que apenas o Chrome suporta o OffscreenCanvas .

Somente o Chrome suporta OffscreenCanvas
Suporte para telas para abril de 2019, de acordo com Can I Use

Mas mesmo aqui, diante do principal inimigo do desenvolvedor da web, o suporte ao navegador, não devemos desistir. Nos reunimos e encontramos o último elemento do quebra-cabeça - este é um caso ideal para "melhoria progressiva". No Chrome e em navegadores futuros, removeremos o friso e outros navegadores funcionarão como antes.

Como resultado, precisaremos escrever um arquivo que possa funcionar em dois ambientes diferentes ao mesmo tempo - em um trabalhador da Web e em um fluxo JS principal comum.

Solução


Para ocultar os hacks sob uma camada de açúcar, criei uma pequena biblioteca JS de tela fora da tela de 400 bytes (!). Nos exemplos, o código o usará, mas vou lhe dizer como ele funciona "sob o capô".

Vamos começar instalando a biblioteca:

 npm install offscreen-canvas 

Precisamos de um arquivo JS separado para o trabalhador da Web - crie um arquivo de montagem separado no Webpack ou Parcel:

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

Os coletores mudam constantemente o nome do arquivo durante a implantação devido aos busters de cache - precisamos escrever o nome em HTML usando a tag de pré - carregamento . Aqui o exemplo será abstrato, pois o código real dependerá muito dos recursos do seu assembly.

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

Agora precisamos obter o nó DOM para as <canvas> e o conteúdo da tag preload no arquivo 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 se houver canvas.transferControlToOffscreen carregará o arquivo JS no trabalhador da web. E na ausência deste método - como um <script> regular.

Crie este webgl-worker.js para o trabalhador:

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

insideWorker verifica se foi carregado dentro de um trabalhador da Web. Dependendo do ambiente, ele lançará diferentes sistemas de comunicação com o thread principal.

A biblioteca executará uma função passada para insideWorker para cada nova mensagem do encadeamento principal. Imediatamente após o carregamento, o createWorker enviará a primeira mensagem { canvas, width, height } para desenhar o primeiro quadro em <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() } }) 

Ao portar o código antigo do Three.js para um trabalhador da Web, você poderá ver erros, pois o trabalhador da Web não possui uma API DOM. Por exemplo, não há document.createElement para carregar texturas SVG. Portanto, às vezes precisaremos de carregadores diferentes em um trabalhador da Web e dentro de um script regular. Para verificar o tipo de ambiente, temos o 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() 

Nós desenhamos o primeiro quadro. Mas a maioria das cenas do WebGL deve responder às ações do usuário. Por exemplo, gire a câmera quando o cursor se mover ou desenhe um quadro quando a janela for redimensionada. Infelizmente, o trabalhador da Web não pode ouvir eventos DOM. Precisamos ouvi-los no fluxo principal e enviar mensagens para o web worker.

  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


Com o OffscreenCanvas derrotei frisos no meu site e ganhei 100% de pontos no Google Lighthouse. E o WebGL funciona em todos os navegadores, mesmo sem o suporte do OffscreenCanvas .

Você pode dar uma olhada no site ativo e no código-fonte do thread ou trabalhador principal .


Com o OffscreenCanvas, os óculos do Google Lighthouse aumentaram de 95 para 100

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


All Articles