
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 imagens de 170K - Eddie OsmaniEnquanto 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
.
Suporte para telas para abril de 2019, de acordo com Can I UseMas 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) {
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
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