TuBrief
구독 채널
비디오
커뮤니티

Como separar o loop de renderização para evitar quedas de quadros ao usar React e WebGL juntos

TuBrief 편집팀
2026년 8월 7일
0
Computing/Software

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

Português한국어EnglishEspañol中文हिन्दीDeutschFrançaisРусскийBahasa Indonesia日本語العربية

관련 영상

Esta biblioteca de componentes de UI é INCRÍVEL (Canvas UI)7:27

Esta biblioteca de componentes de UI é INCRÍVEL (Canvas UI)

Better Stack

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

Como separar o loop de renderização para evitar quedas de quadros ao usar React e WebGL juntos

Adicionar um canvas WebGL a um painel (dashboard) do Next.js é mais complicado do que parece. Quando você adiciona gráficos chamativos ou elementos pesados, a tela frequentemente trava. Se os quadros caem só de mover o mouse e o painel construído com tanto esforço fica lento, é frustrante para qualquer desenvolvedor.

Esse problema geralmente ocorre porque o modelo de gerenciamento de estado do React entra em conflito com o método de renderização do WebGL. Se você não separar a forma como os dois motores trabalham, a tela continuará travando, não importa quão boa seja a biblioteca utilizada.

O problema da biblioteca de gráficos incluída inteiramente no pacote principal

Bibliotecas como Three.js ou Deck.gl são pesadas porque contêm motores de cálculo matemático e compiladores de shader em seu interior. Se você importar o pacote no topo sem pensar, até o código que não está sendo usado acaba misturado no pacote principal.

No ambiente do Next.js, você precisa aliviar esse peso combinando importações diretas de subpaths (subpaths) com a configuração de carregamento dinâmico (dynamic load). Isso também ajuda a evitar que o erro window is not defined ocorra durante a renderização do lado do servidor (SSR).

`javascript
// next.config.mjs
import withBundleAnalyzer from '@next/bundle-analyzer';

const bundleAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
optimizePackageImports: [
'@radix-ui/react-icons',
'lucide-react',
'three',
'deck.gl'
],
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
};
}
return config;
},
};

export default bundleAnalyzer(nextConfig);

`

Ao passar por essa configuração, os recursos gráficos não são baixados de uma só vez no carregamento inicial da página, mas divididos conforme a necessidade. Verificando com o analisador de pacotes (bundle analyzer), você pode ver que o código relacionado ao canvas, que antes estava preso no arquivo JS principal, foi separado em um bloco independente.

No wrapper do componente cliente, desative o SSR ao carregar.

`typescript
// components/dashboard/CanvasDashboardWrapper.tsx
'use client';

import dynamic from 'next/dynamic';
import { Skeleton } from '@/components/ui/skeleton';

const DynamicCanvasRenderer = dynamic(
() => import('./CanvasRenderer').then((mod) => mod.CanvasRenderer),
{
ssr: false,
loading: () => (




),
}
);

export function CanvasDashboardWrapper() {
return (




);
}

`

Configurar dessa forma traz três efeitos imediatos:

  1. Registramos o pacote em next.config.mjs para induzir a omissão de módulos desnecessários.
  2. Passamos { ssr: false } para next/dynamic para bloquear erros de tempo de execução durante a renderização do servidor.
  3. Exibimos um Skeleton de altura fixa para evitar que o layout sofra trancos antes que os dados sejam carregados.

Se você usa Tailwind CSS, também precisa verificar os eventos de ponteiro (pointer events). Aplique pointer-events-none em todo o wrapper do canvas para fazer com que os eventos passem para o DOM traseiro, e aplique pointer-events-auto novamente apenas na camada de objetos que realmente precisa de manipulação.

Removendo o useState de dentro do loop do canvas

O React redesenha o DOM virtual quando os estados (useState, useContext) mudam. Por outro lado, o WebGL redesenha a tela 60 vezes por segundo usando requestAnimationFrame.

O que acontece se chamarmos a função de alteração de estado do React dentro de um loop executado 60 vezes por segundo? A operação de reconciliação (Reconciliation) do React é executada a cada frame, congelando a thread principal. Esse é o verdadeiro motivo pelo qual a tela fica lenta.

Os valores que mudam devem ser armazenados em useRef, e a tarefa de desenhar a tela deve ser deixada para o loop exclusivo de requestAnimationFrame.

`typescript
// hooks/useAnimationFrame.ts
import { useEffect, useRef } from 'react';

type AnimationCallback = (deltaTime: number, timestamp: number) => void;

export const useAnimationFrame = (callback: AnimationCallback, isPaused: boolean = false) => {
const requestRef = useRef<number | null>(null);
const previousTimeRef = useRef<number | null>(null);
const callbackRef = useRef(callback);

useEffect(() => {
callbackRef.current = callback;
}, [callback]);

useEffect(() => {
if (isPaused) {
if (requestRef.current !== null) {
cancelAnimationFrame(requestRef.current);
}
return;
}

const animate = (timestamp: number) => {
  if (previousTimeRef.current !== null) {
    const deltaTime = timestamp - previousTimeRef.current;
    callbackRef.current(deltaTime, timestamp);
  }
  previousTimeRef.current = timestamp;
  requestRef.current = requestAnimationFrame(animate);
};

requestRef.current = requestAnimationFrame(animate);

return () => {
  if (requestRef.current !== null) {
    cancelAnimationFrame(requestRef.current);
  }
};

}, [isPaused]);
};

`

O mesmo vale para eventos frequentes, como movimento do mouse ou arrastar (drag). Se você atualizar o estado do React no manipulador de eventos, a coleta de lixo (garbage collection) ocorrerá em massa em um curto espaço de tempo, causando travamentos (lag).

Vamos mudar a estrutura:

  1. Dados que mudam com frequência, como a posição do mouse ou o valor de rotação, são sobrescritos em current do useRef.
  2. Criamos um loop requestAnimationFrame exclusivo usando o hook useAnimationFrame.
  3. A função de renderização apenas lê esse valor de Ref e o desenha no canvas. Como ela não mexe no estado do React, nenhuma re-renderização é acionada.

Tratamento de fallback para navegadores sem suporte

Assim como a especificação HTML in Canvas em discussão no WICG, há tentativas de desenhar o DOM diretamente no bitmap do canvas usando métodos como drawElementImage. Isso funciona bem em ambientes com flags recentes do Chrome, mas se você confiar cegamente nisso em um ambiente corporativo real, a tela falhará completamente em navegadores antigos ou em alguns dispositivos móveis.

Verifique primeiro se há suporte para os recursos e utilize um limite de erro (Error Boundary) para se preparar para situações excepcionais.

`typescript
// components/canvas/CanvasErrorBoundary.tsx
'use client';

import React, { Component, ErrorInfo, ReactNode } from 'react';
import { detectCanvasCapabilities } from '@/utils/canvasFeatureDetection';

interface Props {
children: ReactNode;
fallbackUI: ReactNode;
}

interface State {
hasError: boolean;
isSupported: boolean;
}

export class CanvasErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
isSupported: true,
};

public componentDidMount() {
const capabilities = detectCanvasCapabilities();
if (!capabilities.webgl2) {
this.setState({ isSupported: false });
}
}

public static getDerivedStateFromError(_: Error): Partial {
return { hasError: true };
}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Canvas UI Rendering Engine Crashed:', error, errorInfo);
}

public render() {
if (this.state.hasError || !this.state.isSupported) {
return (



{this.props.fallbackUI}


);
}

return (
  <div className="relative w-full h-full min-h-[400px] overflow-hidden">
    {this.props.children}
  </div>
);

}
}

`

Aqui está como configurar uma rede de segurança:

  1. Verifique o estado de suporte ao WebGL2 usando a lógica de verificação de contexto do canvas.
  2. Capture erros de tempo de execução que ocorrem durante a renderização usando um Error Boundary baseado em classe do React.
  3. Fixe uma altura mínima como min-h-[400px] na caixa do wrapper para evitar que a proporção da tela quebre ao alternar para a UI de fallback (como gráficos HTML/SVG normais).

O coletor de lixo do JavaScript não limpa a memória da GPU

O motor V8 limpa bem os objetos que estão no heap do JavaScript. No entanto, ele desconhece os dados armazenados na memória da GPU, como buffers WebGL, texturas e programas de shader.

Se você não limpar explicitamente esses recursos ao navegar entre páginas ou alternar abas do painel, a memória da GPU continuará enchendo até que a aba do navegador feche devido a um erro de Context lost.

`typescript
// hooks/useWebGLCleanUp.ts
import { useEffect, useRef } from 'react';

export const useWebGLCleanUp = () => {
const glRef = useRef<WebGL2RenderingContext | null>(null);
const resourcesRef = useRef<{
buffers: WebGLBuffer[];
textures: WebGLTexture[];
programs: WebGLProgram[];
}>({
buffers: [],
textures: [],
programs: [],
});

const registerBuffer = (buffer: WebGLBuffer) => resourcesRef.current.buffers.push(buffer);
const registerTexture = (texture: WebGLTexture) => resourcesRef.current.textures.push(texture);
const registerProgram = (program: WebGLProgram) => resourcesRef.current.programs.push(program);

useEffect(() => {
return () => {
const gl = glRef.current;
if (!gl) return;

  resourcesRef.current.buffers.forEach((buffer) => gl.deleteBuffer(buffer));
  resourcesRef.current.textures.forEach((texture) => gl.deleteTexture(texture));

  resourcesRef.current.programs.forEach((program) => {
    const shaders = gl.getAttachedShaders(program);
    if (shaders) {
      shaders.forEach((shader) => {
        gl.detachShader(program, shader);
        gl.deleteShader(shader);
      });
    }
    gl.deleteProgram(program);
  });

  const loseContextExt = gl.getExtension('WEBGL_lose_context');
  if (loseContextExt) {
    loseContextExt.loseContext();
  }

  resourcesRef.current = { buffers: [], textures: [], programs: [] };
  glRef.current = null;
};

}, []);

return { glRef, registerBuffer, registerTexture, registerProgram };
};

`

Aqui está a sequência para verificar diretamente nas Ferramentas do Desenvolvedor do Chrome se a memória está sendo liberada corretamente:

  1. Abra o componente do painel e tire o primeiro snapshot do heap (S1) na aba Memory.
  2. Repita a montagem e a desmontagem do componente (como navegar para outras abas) mais de 10 vezes.
  3. Clique na coleta manual de lixo (ícone de lixeira) e tire o segundo snapshot do heap (S2).
  4. Defina o alvo de comparação de S2 como S1 e verifique se a contagem de Detached HTMLCanvasElement ou WebGLBuffer caiu para 0 sem deixar resíduos.

Ao usar React e WebGL juntos, o segredo é mantê-los funcionando sem interferir nas respectivas áreas de excelência de cada biblioteca. Dividir o pacote, remover o estado do React do loop de renderização e liberar com segurança a memória da GPU quando o componente desaparecer. Apenas seguindo esses três pontos, você conseguirá criar um painel que roda perfeitamente sem quedas de quadros.