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

Разделение циклов рендеринга для предотвращения падения кадров при совместном использовании React и WebGL

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

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

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

관련 영상

Эта библиотека UI-компонентов ПРОСТО СУПЕР (Canvas UI)7:27

Эта библиотека UI-компонентов ПРОСТО СУПЕР (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
구독 채널
비디오
커뮤니티
로그인

Разделение циклов рендеринга для предотвращения падения кадров при совместном использовании React и WebGL

Добавление холста WebGL в панель управления Next.js оказывается сложнее, чем кажется на первый взгляд. При добавлении ярких диаграмм или тяжелых графических элементов экран часто начинает подтормаживать. Стоит лишь пошевелить мышкой, как падает частота кадров, и разработчику остается только расстраиваться из-за лагов в созданном с таким трудом дашборде.

Эта проблема обычно возникает из-за конфликта между подходом React к управлению состоянием и способом рендеринга WebGL. Если не разделить принципы работы этих двух движков, экран будет продолжать зависать независимо от используемых библиотек.

Проблема попадания всей графической библиотеки в основной бандл

Такие библиотеки, как Three.js или Deck.gl, имеют большой объем, поскольку содержат внутри себя движки математических расчетов и компиляторы шейдеров. Если бездумно импортировать пакет сверху, в основной бандл попадет даже тот код, который вовсе не используется.

В среде Next.js эту проблему следует решать сочетанием прямого импорта подпутей (subpath imports) и настройки динамической загрузки. Это также предотвратит появление ошибки window is not defined во время серверного рендеринга.

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

`

После применения этой настройки графические ресурсы не загружаются целиком во время начальной загрузки страницы, а подгружаются по мере необходимости. Проверив с помощью анализатора бандлов, можно увидеть, что связанный с холстом код, который раньше был частью основного файла JS, отделился в самостоятельный чанк.

В обертке клиентского компонента отключите SSR при импорте.

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




);
}

`

Такая конфигурация сразу дает три преимущества:

  1. Регистрация пакетов в next.config.mjs позволяет исключить неиспользуемые модули.
  2. Передача { ssr: false } в next/dynamic предотвращает ошибки времени выполнения при серверном рендеринге.
  3. Отображение компонента Skeleton фиксированной высоты устраняет подпрыгивание макета до загрузки данных.

Если вы используете Tailwind CSS, также необходимо проверить указатели мыши (pointer events). Добавьте pointer-events-none ко всей обертке холста, чтобы события проходили к расположенному ниже DOM, и верните pointer-events-auto только тем слоям объектов, которым требуется реальное взаимодействие.

Уберите useState из цикла отрисовки холста

При изменении состояния в React (useState, useContext) происходит перерисовка виртуального DOM. WebGL же перерисовывает экран 60 раз в секунду с помощью requestAnimationFrame.

Что произойдет, если вызывать функцию изменения состояния React внутри цикла, выполняющегося 60 раз в секунду? Каждый кадр запускаются операции согласования (reconciliation) в React, что приводит к замерзанию основного потока. Именно в этом кроется истинная причина зависаний интерфейса.

Изменяющиеся значения следует хранить в useRef, а задачу прорисовки экрана стоит доверить отдельному циклу 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]);
};

`

Часто возникающие события, такие как перемещение мыши или перетаскивание, работают точно так же. Обновление состояния React в обработчиках событий приводит к лавинообразному выделению памяти сборщиком мусора за короткое время и вызывает лаги.

Изменим структуру:

  1. Данные, которые часто меняются (например, позиция мыши или значения вращения), записываются в current объекта useRef.
  2. С помощью хука useAnimationFrame создается собственный цикл requestAnimationFrame.
  3. Функция рендеринга просто считывает эти значения из Ref и рисует их на холсте. Поскольку состояние React не задействовано, повторные ре-рендеры не происходят вовсе.

Обработка фоллбеков для неподдерживаемых браузеров

Существуют попытки напрямую отрисовывать DOM в растровое изображение холста с помощью таких методов, как drawElementImage, по аналогии со спецификацией HTML in Canvas, обсуждаемой в WICG. Это отлично работает в среде со свежими флагами Chrome, но если положиться на это в реальных проектах, на старых браузерах или некоторых мобильных устройствах экран полностью пропадет.

Сначала проверяется поддержка функций, а затем код оборачивается в границу ошибок (error boundary) для подготовки к исключительным ситуациям.

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

}
}

`

Способ создания защитного механизма:

  1. Проверяется состояние поддержки WebGL2 с помощью логики проверки контекста холста.
  2. Ошибки времени выполнения, возникающие во время рендеринга, перехватываются с помощью классового компонента Error Boundary в React.
  3. Фиксация минимальной высоты, например min-h-[400px], у контейнера предотвращает искажение пропорций экрана при переключении на фоллбек-интерфейс (обычные графики HTML/SVG и т.д.).

Сборщик мусора JavaScript не очищает память графического процессора

Движок V8 отлично справляется с очисткой объектов в куче JavaScript. Однако он ничего не знает о данных, размещенных в памяти GPU, таких как буферы WebGL, текстуры или программы шейдеров.

Если явно не удалять эти ресурсы при переходе между страницами или переключении вкладок дашборда, память GPU будет заполняться, пока браузер не выдаст ошибку 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.buffers.forEach((buffer) => gl.deleteBuffer(buffer)); // Примечание: исправление возможной опечатки в оригинале или оставление оригинального кода
  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 };
};

`

Порядок проверки правильности освобождения памяти с помощью инструментов разработчика Chrome:

  1. Откройте компонент дашборда и сделайте первый снимок кучи (S1) на вкладке Memory.
  2. Повторите монтирование и размонтирование компонента (переход между другими вкладками и т.д.) более 10 раз.
  3. Нажмите кнопку ручной сборки мусора (иконка корзины) и сделайте второй снимок кучи (S2).
  4. Сравните S2 с S1 и убедитесь, что количество объектов Detached HTMLCanvasElement или WebGLBuffer упало до нуля без утечек.

Главное при совместном использовании React и WebGL — дать каждой библиотеке выполнять свою работу в ее сильных областях без взаимных помех. Разделение бандла, удаление состояния React из цикла рендеринга и гарантированная очистка памяти GPU при демонтировании компонента. Выполнение всего трех этих пунктов позволит создать дашборд, который работает плавно и без падения кадров.