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의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
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.
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:
next.config.mjs para induzir a omissão de módulos desnecessários.{ ssr: false } para next/dynamic para bloquear erros de tempo de execução durante a renderização do servidor.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.
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:
current do useRef.requestAnimationFrame exclusivo usando o hook useAnimationFrame.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 (
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:
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 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:
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.