Класична розробка 3D-графіки у вебі на базі бібліотеки Three.js будується на строго імперативній моделі. Розробник вручну створює сцену (new THREE.Scene()), налаштовує перспективну камеру, підключає WebGLRenderer до HTML-елемента Canvas та запускає нескінченний цикл рендерингу через requestAnimationFrame.
У масштабних інтерфейсах застосунків такий підхід породжує критичні архітектурні проблеми:
WebGL Context Lost.setState на кожен кадр) призводить до просідання частоти кадрів зі 120 FPS до переривчастих 5–10 FPS.Архітектурним рішенням є React Three Fiber (R3F). R3F — це не просто бібліотека-обгортка, а повноцінний платформений реконсилятор, що поєднує декларативну композицію React із максимальною продуктивністю апаратного WebGL/WebGPU.
R3F переносить фундаментальні принципи компонентної архітектури React (композиція, Suspense, хуки, типізація) у світ тривимірної графіки. При цьому високочастотний цикл анімації (60–120 FPS) ізолюється в спеціалізованому хуку useFrame, який безпосередньо мутує матриці трансформацій об'єктів Three.js за посиланням без жодного повторного рендерингу компонентів React.
Архітектура R3F ґрунтується на строгому розділенні двох паралельних контурів виконання: контуру узгодження React Fiber та високочастотного графічного циклу WebGL:
┌─────────────────────────────────────────────────────────────┐
│ REACT FIBER RECONCILER (JSX Layer) │
│ <Canvas> │
│ <ambientLight intensity={0.5} /> │
│ <mesh ref={meshRef}> │
│ <boxGeometry args={[1, 1, 1]} /> │
│ <meshStandardMaterial color="#8B5CF6" /> │
│ </mesh> │
│ </Canvas> │
└──────────────────────────────┬──────────────────────────────┘
│
▼ (R3F HostConfig: createInstance)
┌─────────────────────────────────────────────────────────────┐
│ THREE.JS SCENE GRAPH (Native 3D Objects) │
│ const scene = new THREE.Scene(); │
│ const mesh = new THREE.Mesh(geometry, material); │
│ scene.add(mesh); │
└──────────────────────────────┬──────────────────────────────┘
│
▼ (useFrame Loop: 60/120 FPS)
┌─────────────────────────────────────────────────────────────┐
│ HARDWARE GPU PIPELINE (Compositor & VRAM) │
│ mesh.rotation.y += delta; // Zero React Reconciliations! │
│ gl.drawElements(GL_TRIANGLES, ...); │
└─────────────────────────────────────────────────────────────┘Canvas: Кореневий компонент-міст. Створює HTML-елемент <canvas>, ініціалізує WebGL-контекст, камеру, сцену та монтує віртуальне дерево реконсилятора R3F.useFrame виконуються прямою мутацією об'єктів Three.js за посиланням ref без виклику setState..dispose() при розмонтуванні JSX-вузлів, запобігаючи витокам пам'яті.Компонент Canvas створює сцену THREE.Scene, перспективну камеру та налаштовує WebGLRenderer з обмеженням dpr={[1, 2]}.
R3F Reconciler транслює рядкові JSX-теги (<mesh>, <boxGeometry>) у нативні об'єкти Three.js і зв'язує їх через attach.
Хук useGLTF завантажує бінарні 3D-асети у фоновому воркері та координує відображення через React 19 Suspense.
Хук useFrame покадрово оновлює матриці трансформацій з множенням на delta time без ререндерів React Fiber.
Розробник анімує обертання тривимірного меша і помістив виклик setRotation(r => r + 0.01) всередину хука useFrame. До яких наслідків для продуктивності застосунку це призведе?
Повноекранний огляд товарів (автомобілі, меблі, взуття, преміальна електроніка) з динамічною зміною матеріалів, текстур та кольору в реальному часі за кліками по стандартних UI-кнопках React.
Сцени з тисячами інтерактивних частинок (InstancedMesh), що реагують на рух курсора миші, з просторовим паралаксом камери та кінематографічною постобробкою (Bloom, Depth of Field).
Завантаження оптимізованих моделей складних механізмів та архітектурних споруд у форматі GLTF/GLB із можливістю кліку по окремих деталях (Raycasting) для виведення технічної інформації.
Процедурні хвильові поверхні та спектрограми на базі кастомних шейдерів GLSL, керовані вхідними WebSocket-подіями або потоком Web Audio API.
При перемиканні моделей або текстур збирач сміття V8 видаляє лише посилання на об'єкти в оперативній пам'яті (RAM), тоді як важкі вершинні буфери (VBO) та текстурні карти залишаються заблокованими у виділеній пам'яті GPU (VRAM).
У R3F вбудовано автоматичний виклик методу .dispose() при видаленні декларативних тегів JSX. Однак при процедурному створенні матеріалів і геометрій потрібен детермінований обхід і очищення:
// Очищення фізичних ресурсів меша з відеопам'яті GPU
export function disposeHierarchy(object: THREE.Object3D): void {
object.traverse((child) => {
if ('geometry' in child && child.geometry instanceof THREE.BufferGeometry) {
child.geometry.dispose();
}
if ('material' in child) {
const mat = child.material;
if (Array.isArray(mat)) {
mat.forEach((m) => m.dispose());
} else if (mat instanceof THREE.Material) {
mat.dispose();
}
}
});
}Чому при налаштуванні кореневого компонента Canvas критично важливо явно вказувати проп dpr={[1, 2]} замість використання системного window.devicePixelRatio?
Побудуємо закінчену промислову 3D-підсистему для React 19:
src/components/3d/ProductViewer3D.tsx: Відмовостійкий інтерактивний 3D-переглядач із захистом від витоків VRAM, обмеженням dpr={[1, 1.5]}, м'якими контактними тінями та підтримкою React 19 <Suspense>.src/components/3d/InteractiveMeshNode.tsx: Ізольований тривимірний вузол з реакцією на курсор миші, кінетичним обертанням через useFrame з компенсацією delta та вибором матеріалу.'use client';
import * as React from 'react';
import { useFrame } from '@react-three/fiber';
import type { Mesh } from 'three';
export interface InteractiveMeshNodeProps {
readonly wireframe: boolean;
readonly baseColor: string;
}
export function InteractiveMeshNode({ wireframe, baseColor }: InteractiveMeshNodeProps) {
// Посилання на нативний інстанс THREE.Mesh для високочастотної мутації
const meshRef = React.useRef<Mesh>(null);
const [isHovered, setIsHovered] = React.useState(false);
// Цикл кадру WebGL: 60/120 FPS без виклику ререндера React
useFrame((state, delta) => {
if (!meshRef.current) return;
// Обертання з урахуванням часу delta (однакова швидкість на 60 Гц та 144 Гц)
const speedMultiplier = isHovered ? 1.2 : 0.4;
meshRef.current.rotation.y += delta * speedMultiplier;
meshRef.current.rotation.x += delta * (speedMultiplier * 0.5);
// Плавне синусоїдальне погойдування по висоті
meshRef.current.position.y = 0.5 + Math.sin(state.clock.elapsedTime * 1.5) * 0.15;
});
return (
<mesh
ref={meshRef}
position={[0, 0.5, 0]}
castShadow
receiveShadow
onPointerOver={(e) => {
e.stopPropagation();
setIsHovered(true);
}}
onPointerOut={() => setIsHovered(false)}
>
{/* Тороїдальний вузол високої щільності полігонів */}
<torusKnotGeometry args={[1, 0.32, 128, 32]} />
<meshStandardMaterial
color={isHovered ? '#06B6D4' : baseColor}
roughness={0.15}
metalness={0.85}
wireframe={wireframe}
/>
</mesh>
);
}'use client';
import * as React from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, ContactShadows, Environment } from '@react-three/drei';
import { InteractiveMeshNode } from './InteractiveMeshNode';
export function ProductViewer3D() {
const [wireframe, setWireframe] = React.useState(false);
const [color, setColor] = React.useState('#8B5CF6');
return (
<div className="relative w-full max-w-2xl mx-auto rounded-3xl overflow-hidden bg-[#0A0A12] border border-border shadow-2xl">
{/* 3D Canvas Контейнер */}
<div className="w-full h-[420px]">
<Canvas
shadows
camera={{ position: [0, 1.8, 4.2], fov: 45 }}
dpr={[1, 1.5]} // Захист GPU від тротлінгу на 4K/Retina екранах
gl={{
powerPreference: 'high-performance',
antialias: true,
}}
>
{/* Фоновий колір сцени */}
<color attach="background" args={['#0A0A12']} />
<React.Suspense fallback={null}>
{/* Реалістичне HDR-освітлення оточення */}
<Environment preset="city" />
{/* Тривимірна деталь */}
<InteractiveMeshNode wireframe={wireframe} baseColor={color} />
{/* М'які контактні тіні під об'єктом */}
<ContactShadows
position={[0, -0.6, 0]}
opacity={0.65}
scale={8}
blur={2.2}
far={4}
/>
</React.Suspense>
{/* Інтерактивне керування камерою */}
<OrbitControls
enablePan={false}
minDistance={2.5}
maxDistance={7}
minPolarAngle={Math.PI / 6}
maxPolarAngle={Math.PI / 2}
/>
</Canvas>
</div>
{/* Панель керування параметрами моделі */}
<div className="p-4 bg-bg-surface/90 border-t border-border backdrop-blur-md flex items-center justify-between">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setColor('#8B5CF6')}
className={`w-6 h-6 rounded-full bg-[#8B5CF6] border-2 transition-transform cursor-pointer ${
color === '#8B5CF6' ? 'scale-110 border-white' : 'border-transparent'
}`}
aria-label="Фіолетовий колір"
/>
<button
type="button"
onClick={() => setColor('#06B6D4')}
className={`w-6 h-6 rounded-full bg-[#06B6D4] border-2 transition-transform cursor-pointer ${
color === '#06B6D4' ? 'scale-110 border-white' : 'border-transparent'
}`}
aria-label="Блакитний колір"
/>
<button
type="button"
onClick={() => setColor('#10B981')}
className={`w-6 h-6 rounded-full bg-[#10B981] border-2 transition-transform cursor-pointer ${
color === '#10B981' ? 'scale-110 border-white' : 'border-transparent'
}`}
aria-label="Смарагдовий колір"
/>
</div>
<button
type="button"
onClick={() => setWireframe(!wireframe)}
className={`px-3 py-1.5 text-xs font-mono font-semibold rounded-xl border transition-colors cursor-pointer ${
wireframe
? 'bg-accent-violet text-white border-accent-violet'
: 'bg-bg-elevated text-text-muted border-border hover:text-text-primary'
}`}
>
{wireframe ? 'Каркас: УВІМК' : 'Каркас: ВИМК'}
</button>
</div>
{/* Інформаційний бейдж специфікації */}
<div className="absolute top-4 left-4 pointer-events-none text-[10px] font-mono font-bold uppercase tracking-wider text-text-muted bg-black/60 px-2.5 py-1 rounded-md border border-white/10 backdrop-blur-sm">
R3F WebGL 2.0 | 120 FPS GPU Loop
</div>
</div>
);
}Виклик функції оновлення стану всередині useFrame призводить до того, що компонент ререндериться 60–120 разів на секунду. Це миттєво паралізує Event Loop браузера та обвалює продуктивність інтерфейсу.
Рішення: Мутуйте тривимірні властивості тільки напряму через нативний Three.js-об'єкт: meshRef.current.rotation.y += delta.
Створення нових екземплярів new THREE.BoxGeometry() у тілі компонента на кожен рендер без виклику методу .dispose() забиває виділену відеопам'ять GPU, призводячи до помилки WebGL Context Lost.
Рішення: Використовуйте декларативні теги <boxGeometry /> (R3F утилізує їх автоматично) або вручну викликайте geometry.dispose() у cleanup-функції useEffect.
За замовчуванням полотно може ініціалізуватися з фізичним window.devicePixelRatio = 3. Це потроює число пікселів і підвищує навантаження на фрагментні шейдери в 9 разів, викликаючи перегрів мобільних пристроїв.
Рішення: Завжди явно обмежуйте щільність пікселів: <Canvas dpr={[1, 1.5]} />.
Якщо шейдер або текстура не має атрибута attach, R3F не зможе визначити, до якої властивості батьківського об'єкта її прикріпити, і спробує додати її як дочірній меш.
Рішення: Явно декларуйте слот монтування: <primitive object={texture} attach="map" />.
useFrame через пряму мутацію полів за посиланням ref без виклику setState..dispose().dpr={[1, 1.5]} захищає мобільні пристрої від тротлінгу фрагментних шейдерів на Retina та 4K-дисплеях.useGLTF забезпечує декларативне завантаження бінарних 3D-моделей із Draco-декомпресією.