Por que a página com 3D cai de 60 para 37 fps no notebook?

No meu site, tela animada em CanvasTexture derrubou o Three.js de 60 para 37 fps na Intel UHD. willReadFrequently resolveu, e a medição exigiu um controle.

Pedro Henrique Quadro4 min de leitura

Uma textura vinda de um canvas 2D que é atualizada a cada quadro pode custar caro quando o canvas é acelerado pela placa de vídeo. No meu site, três telas animadas de um celular em 3D derrubaram a cena de 60 para 37 fps no notebook. Criar o canvas com willReadFrequently: true devolveu os 60 fps. Fps é a quantidade de quadros desenhados por segundo, e 60 é o que deixa o movimento fluido.

O que a documentação diz

Conferi as fontes em 10 de outubro de 2026. No Three.js, uma CanvasTexture cria uma textura (a imagem que reveste um objeto 3D) a partir de um elemento canvas. A documentação diz que ela liga o needsUpdate logo na criação, porque o canvas pode ser usado direto na renderização. Ao desenhar de novo no canvas, é preciso avisar com texture.needsUpdate = true para a imagem ser enviada à GPU, o chip gráfico, outra vez.

A MDN descreve a opção willReadFrequently do contexto 2D como um aviso de que haverá muitas leituras de pixels. Ela diz que isso "força o uso de um canvas 2D por software", em vez de acelerado por hardware, e que pode economizar memória ao chamar getImageData() com frequência. A MDN não diz nada sobre texturas do Three.js nem sobre desempenho em WebGL. A relação entre uma coisa e outra é interpretação minha, com base no teste abaixo.

O que vi no meu site

Medi no protótipo da página de abertura do meu site, com Three.js r170 e o Chromium usando a GPU real do notebook, uma Intel UHD. Uma página em branco, usada como controle na mesma rodada, deu 59 fps. Com três telas de 1.200 pixels atualizadas em rodízio, a cena marcou:

  • 37 fps com o canvas normal.
  • 60 fps sem atualizar as telas.
  • 60 fps com canvas.getContext('2d', { willReadFrequently: true }), em todas as etapas e também rolando a página.

O console do Chromium mostrava a mensagem GPU stall due to ReadPixels, que quer dizer que a GPU ficou parada esperando uma leitura de pixels. Minha leitura: o Chrome desenha o canvas acelerado na GPU e, para usá-lo como textura WebGL, lê os pixels de volta. Não vi isso documentado, é só a explicação que combina com a mensagem e com os números. Desligar o antialias, suspeito óbvio, deu 39 fps, então não era o gargalo.

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d', { willReadFrequently: true })
const textura = new THREE.CanvasTexture(canvas)

// a cada atualização da tela animada
textura.needsUpdate = true

Como medir sem se enganar

A medição oscila. Em 14 de setembro de 2026, a mesma página deu 60 e depois 33 fps, com o notebook sob carga alta e a CPU a 94 °C. Na mesma bateria, a página em branco caiu para 53 fps. Sem o controle, eu teria chamado isso de regressão do código.

Em 10 de outubro de 2026 caí numa segunda armadilha. Meu script abria a página no modo celular, tirava os prints e não fechava. A cena 3D dela continuava rodando em segundo plano no mesmo navegador. Uma outra versão do protótipo caiu de 60 para 45 fps com o controle em 60. Depois de fechar o contexto antes de medir, deu 60 parada e 57 rolando. O controle não denunciou nada, porque foi medido antes de a outra página abrir.

Outro detalhe: o Chromium em modo sem janela usa renderização por software e deu 3 fps, o que nem serve para comparar. Usei --enable-gpu --use-angle=gl --ignore-gpu-blocklist para usar a GPU de verdade.

Um segundo problema, na forma do aparelho

Ao modelar o celular, o aro saiu com canto quase reto, e a tela arredondada por cima denunciava. A documentação do RoundedBoxGeometry lista o raio, com padrão de 0,1, e não fala de nenhum limite. No código da versão r170 há a linha radius = Math.min( width / 2, height / 2, depth / 2, radius ): o raio nunca passa da metade da menor medida. Meu aparelho tinha 0,078 de espessura e pedi raio 0,115, que virou 0,039.

Para um corpo com canto mais redondo, desenhei o contorno com Shape e absarc e extrudei com ExtrudeGeometry, que aceita bisel pelas opções bevelThickness e bevelSize.

O que fazer

  1. Crie canvas de textura animada com willReadFrequently: true e atualize só enquanto a animação daquela tela corre, em rodízio.
  2. Meça sempre com um controle na mesma rodada: uma página em branco, no mesmo navegador e na mesma GPU.
  3. Antes de medir, deixe só uma página WebGL viva. Feche todo contexto aberto para tirar print.
  4. Compare com e sem a mudança, uma página por vez.
  5. Confira o raio efetivo de cantos arredondados, em vez de confiar no valor que você passou.

Se você tem uma página com 3D, animação pesada ou um aplicativo que engasga em notebook comum, veja como eu trabalho com aplicativos.

Fontes

  1. MDN: getContext e willReadFrequently, força canvas 2D por software
  2. Three.js: CanvasTexture, needsUpdate ligado na criação
  3. Three.js: RoundedBoxGeometry, parâmetros e raio (a documentação não fala do limite)
  4. Three.js: código do RoundedBoxGeometry na versão r170, linha que limita o raio
  5. Three.js: ExtrudeGeometry, opções de bisel
Pedro Henrique QuadroEngenheiro de software e IA. Constrói aplicativos, sistemas, painéis e agentes de IA em produção, e tem código aceito no Supabase, no Kestra e no QuestDB.

Continue lendo