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

Separación del bucle de renderizado para evitar la caída de fotogramas al usar React y WebGL juntos

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

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

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

관련 영상

Esta biblioteca de componentes UI es INCREÍBLE (Canvas UI)7:27

Esta biblioteca de componentes UI es INCREÍBLE (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
구독 채널
비디오
커뮤니티
로그인

Separación del bucle de renderizado para evitar la caída de fotogramas al usar React y WebGL juntos

Integrar un lienzo WebGL en un panel de Next.js es más complicado de lo que parece. Al añadir gráficos llamativos o elementos pesados, la pantalla suele entrecortarse. Con solo mover el ratón los fotogramas caen, y si el panel que tanto te costó crear se traba, es frustrante para cualquier desarrollador.

Este problema suele ocurrir porque el enfoque de gestión de estado de React choca con el enfoque de renderizado de WebGL. Si no separas la forma en que trabajan ambos motores, por muy buena que sea la biblioteca que uses, la pantalla seguirá entrecortándose.

El problema de que las bibliotecas gráficas entren por completo en el paquete principal

Bibliotecas como Three.js o Deck.gl tienen un gran tamaño porque incluyen internamente un motor de operaciones matemáticas y un compilador de sombreadores (shaders). Si importas el paquete desde arriba sin pensarlo, hasta el código que ni siquiera usas se mezcla en el paquete principal.

Si estás en un entorno de Next.js, debes combinar las importaciones directas de subrutas y la configuración de carga dinámica para aligerar esta carga. También puedes evitar que explote el error window is not defined durante el renderizado del lado del servidor.

`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);

`

Tras aplicar esta configuración, al cargar la página inicial no recibes todos los recursos gráficos de golpe, sino que los obtienes por partes según los necesitas. Si lo compruebas con el analizador de paquetes, verás que el código relacionado con el lienzo, que antes estaba agrupado en el archivo JS principal, se ha separado en un bloque independiente.

En el contenedor del componente de cliente, desactiva el SSR al importarlo.

`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 (




);
}

`

Al configurarlo de esta manera, obtienes tres efectos inmediatos:

  1. Reduces módulos innecesarios registrando el paquete en next.config.mjs.
  2. Bloqueas errores de tiempo de ejecución durante el renderizado del servidor asignando { ssr: false } a next/dynamic.
  3. Evitas que el diseño salte antes de que lleguen los datos mostrando un Skeleton de altura fija.

Si usas Tailwind CSS, también debes verificar los eventos de puntero. Aplica pointer-events-none a todo el contenedor del lienzo para que los eventos pasen al DOM posterior, y vuelve a dar pointer-events-auto únicamente a la capa de objetos que requiera manipulación real.

Quitar useState del bucle del lienzo

React vuelve a dibujar el DOM virtual cuando cambia el estado (useState, useContext). En cambio, WebGL redibuja la pantalla 60 veces por segundo mediante requestAnimationFrame.

¿Qué pasa si llamas a la función de cambio de estado de React dentro de un bucle que se ejecuta 60 veces por segundo? En cada fotograma se ejecuta el cálculo de reconciliación de React y el hilo principal se bloquea. Esta es la verdadera razón por la que la pantalla se traba.

Los valores cambiantes deben guardarse en useRef, y la tarea de dibujar la pantalla debe dejarse en manos de un bucle 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]);
};

`

Lo mismo ocurre con los eventos frecuentes como el movimiento del ratón o el arrastre. Si actualizas el estado de React desde el manejador de eventos, la recolección de basura se acumula en poco tiempo y se produce retraso.

Cambiemos la estructura:

  1. Los datos que cambian con frecuencia, como la posición del ratón o los valores de rotación, se sobrescriben en el current de useRef.
  2. Se crea un bucle requestAnimationFrame propio con el gancho useAnimationFrame.
  3. La función de renderizado simplemente lee estos valores de Ref y los dibuja en el lienzo. Como no toca el estado de React, no se produce ningún re-renderizado.

Gestión de respaldo (fallback) para navegadores no compatibles

Al igual que la especificación HTML in Canvas que se debate en el WICG, existen intentos de dibujar el DOM directamente en el mapa de bits del lienzo con métodos como drawElementImage. Funciona bien en entornos con flags recientes de Chrome, pero si confías ciegamente en esto en entornos de producción, la pantalla fallará por completo en navegadores antiguos o en algunos dispositivos móviles.

Primero lee si la función es compatible y prepárate para excepciones agrupándola en un límite de errores.

`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>
);

}
}

`

Los pasos para establecer una red de seguridad son:

  1. Comprobar el estado de compatibilidad con WebGL2 mediante la lógica de verificación del contexto del lienzo.
  2. Capturar los errores de tiempo de ejecución que ocurren durante el renderizado con un límite de errores basado en clases de React.
  3. Fijar una altura mínima como min-h-[400px] en la caja contenedora para evitar que la relación de aspecto de la pantalla se rompa al cambiar a una interfaz de respaldo (como gráficos HTML/SVG estándar).

El recolector de basura de JavaScript no limpia la memoria de la GPU

El motor V8 limpia muy bien los objetos que se encuentran en el montón (heap) de JavaScript. Sin embargo, no sabe nada sobre los datos colocados en la memoria de la GPU, como los búferes de WebGL, las texturas o los programas de sombreadores.

Si no borras explícitamente estos recursos al navegar entre páginas o cambiar pestañas del panel, la memoria de la GPU se llenará y la pestaña del navegador terminará cerrándose con un error 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 };
};

`

Esta es la secuencia para comprobar directamente con las Herramientas de desarrollador de Chrome si la memoria se libera correctamente:

  1. Abre el componente del panel y toma la primera instantánea del montón (S1) en la pestaña Memory.
  2. Repite el montaje y desmontaje del componente (como cambiar de pestaña) 10 veces o más.
  3. Presiona la recolección de basura manual (icono de papelera) y luego toma la segunda instantánea del montón (S2).
  4. Configura S2 para compararlo con S1 y verifica que la cantidad de elementos como Detached HTMLCanvasElement o WebGLBuffer descienda a 0 y no queden restos.

Al usar React y WebGL juntos, la clave es mantenerlos estables sin dañar las áreas en las que cada biblioteca destaca. Dividir el paquete, eliminar el estado de React del bucle de renderizado y vaciar con seguridad la memoria de la GPU cuando el componente desaparece. Siguiendo estos tres puntos podrás crear un panel que funcione de manera impecable y sin caídas de fotogramas.