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

Trennen der Rendering-Schleife zur Vermeidung von Framedrops bei der Verwendung von React und WebGL

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

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

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

관련 영상

Diese UI-Komponentenbibliothek ist GEWALTIG (Canvas UI)7:27

Diese UI-Komponentenbibliothek ist GEWALTIG (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
구독 채널
비디오
커뮤니티
로그인

Trennen der Rendering-Schleife zur Vermeidung von Framedrops bei der Verwendung von React und WebGL

Das Einbinden einer WebGL-Canvas in ein Next.js-Dashboard ist kniffliger als gedacht. Bei bunten Charts oder schweren Grafikelementen fängt die Anzeige schnell an zu stottern. Schon das Bewegen der Maus lässt die Framerate einbrechen, und wenn das mühsam erstellte Dashboard ruckelt, ist das für Entwickler natürlich frustrierend.

Dieses Problem entsteht meist dadurch, dass die State-Management-Logik von React und die Rendering-Logik von WebGL miteinander kollidieren. Wenn man die Arbeitsweisen dieser beiden Engines nicht trennt, stottert das Bild weiter, egal wie gut die verwendete Bibliothek ist.

Das Problem, dass die Grafikbibliothek komplett im Haupt-Bundle landet

Bibliotheken wie Three.js oder Deck.gl sind recht groß, da sie intern eine mathematische Rechen-Engine und einen Shader-Compiler mitbringen. Wenn man das Paket unüberlegt ganz oben importiert, landet am Ende auch ungenutzter Code im Haupt-Bundle.

In einer Next.js-Umgebung sollte man diese Last durch eine Kombination aus Direktimporten von Subpfaden und dynamischem Laden reduzieren. Dadurch lässt sich gleichzeitig verhindern, dass während des Server-Side-Renderings der Fehler window is not defined auftritt.

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

`

Nach dieser Konfiguration werden die Grafikressourcen beim initialen Laden der Seite nicht mehr auf einmal, sondern bedarfsgerecht geladen. Ein Blick in den Bundle-Analyzer zeigt, dass der zuvor in der Haupt-JS-Datei gebündelte Canvas-Code in einen eigenständigen Chunk ausgelagert wurde.

Der Client-Component-Wrapper wird ohne SSR importiert:

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




);
}

`

Diese Struktur bringt drei sofortige Vorteile:

  1. Durch die Registrierung der Pakete in next.config.mjs wird das Weglassen unnötiger Module gefördert.
  2. Die Angabe von { ssr: false } in next/dynamic blockiert Laufzeitfehler zum Zeitpunkt des Server-Renderings.
  3. Ein Skeleton mit fester Höhe verhindert ein Layout-Springen, bevor die Daten geladen sind.

Bei der Verwendung von Tailwind CSS müssen auch Pointer-Events berücksichtigt werden. Durch das Setzen von pointer-events-none auf den gesamten Canvas-Wrapper werden Ereignisse an das dahinterliegende DOM weitergeleitet, während nur die tatsächlich zu bedienenden Objektschichten wieder pointer-events-auto erhalten.

useState aus der Canvas-Schleife verbannen

Wenn sich in React States (useState, useContext) ändern, wird das Virtual DOM neu gerendert. WebGL hingegen aktualisiert den Bildschirm 60 Mal pro Sekunde über requestAnimationFrame.

Was passiert, wenn man innerhalb einer Schleife, die 60 Mal pro Sekunde läuft, eine React-State-Änderungsfunktion aufruft? Bei jedem Frame greift die Reconciliation-Berechnung von React, wodurch der Main Thread einfriert. Das ist der wahre Grund für ruckelnde Bildschirme.

Sich verändernde Werte sollten in einem useRef gespeichert werden, während die Zeichenaufgabe einer separaten requestAnimationFrame-Schleife überlassen wird.

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

`

Häufige Ereignisse wie Mausbewegungen oder Drag-Aktionen funktionieren genauso. Wenn man im Event-Handler den React-State aktualisiert, kommt es zu einer Ballung der Garbage Collection in kürzester Zeit und somit zu Lags.

Die Struktur wird wie folgt angepasst:

  1. Häufig wechselnde Daten wie Mausposition oder Rotationswerte werden in current eines useRef geschrieben.
  2. Mit dem Hook useAnimationFrame wird eine eigene requestAnimationFrame-Schleife erstellt.
  3. Die Rendering-Funktion liest diesen Ref-Wert aus und zeichnet ihn schlicht auf das Canvas. Da der React-State nicht berührt wird, findet gar kein Re-Rendering statt.

Fallback-Behandlung für nicht unterstützte Browser

Ähnlich wie bei dem von WICG diskutierten HTML-in-Canvas-Standard gibt es Versuche, das DOM mit Methoden wie drawElementImage direkt auf eine Canvas-Bitmap zu zeichnen. Das funktioniert zwar in modernen Chrome-Flag-Umgebungen gut, bricht aber in der Praxis auf älteren Browsern oder bestimmten Mobilgeräten komplett ein.

Zuerst wird die Funktionsunterstützung geprüft und das Ganze mit einem Error Boundary umschlossen, um auf Ausnahmefälle vorbereitet zu sein.

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

}
}

`

So wird ein Sicherheitsnetz gespannt:

  1. Der Status der WebGL2-Unterstützung wird über eine Logik zur Überprüfung des Canvas-Kontexts getestet.
  2. Laufzeitfehler während des Renderings werden von einem React Class Error Boundary abgefangen.
  3. Eine feste Mindesthöhe wie min-h-[400px] im Wrapper-Container verhindert ein Zusammenbrechen des Seitenverhältnisses beim Umschalten auf die Fallback-UI (normale HTML/SVG-Charts etc.).

GPU-Speicher wird nicht vom JavaScript-Garbage-Collector bereinigt

Die V8-Engine räumt Objekte im JavaScript-Heap zuverlässig auf. Sie weiß jedoch nichts von Daten, die im GPU-Speicher abgelegt wurden, wie WebGL-Buffer, Texturen oder Shader-Programme.

Werden diese Ressourcen beim Seitenwechsel oder beim Verlassen eines Dashboard-Tabs nicht explizit gelöscht, läuft der GPU-Speicher voll, bis schließlich ein Context lost-Fehler auftritt und der Browser-Tab abstürzt.

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

`

Die Schritte zur direkten Überprüfung der Speicherbereinigung über die Chrome DevTools:

  1. Das Dashboard-Component öffnen und im Memory-Tab einen ersten Heap-Snapshot (S1) erstellen.
  2. Das Mounten und Unmounten der Component (z. B. durch Wechseln der Tabs) mindestens 10 Mal wiederholen.
  3. Die manuelle Garbage Collection (Mülleimer-Symbol) ausführen und einen zweiten Heap-Snapshot (S2) aufnehmen.
  4. S2 mit S1 vergleichen und sicherstellen, dass keine Detached HTMLCanvasElement- oder WebGLBuffer-Instanzen übrig geblieben sind und die Anzahl auf 0 gesunken ist.

Bei der gemeinsamen Verwendung von React und WebGL ist es der Schlüssel, die jeweiligen Stärken beider Bibliotheken nicht zu beeinträchtigen. Bundles aufteilen, den React-State aus der Rendering-Schleife verbannen und den GPU-Speicher beim Verschwinden einer Component verlässlich leeren. Schon mit diesen drei Schritten lässt sich ein flüssig laufendes Dashboard ganz ohne Framedrops erstellen.