Управління пам'яттю Three.js — dispose(), ліміти WebGL-контексту та виявлення витоків

Кожна геометрія, матеріал і текстура, які ви створюєте в Three.js, виділяють пам'ять GPU. Якщо ви ніколи не викликаєте dispose(), ця пам'ять не звільняється — а браузери обмежують кількість WebGL-контекстів на сторінці 8–16. Це все, що ми з'ясували, керуючи пам'яттю у 350 інтерактивних симуляціях.

Чому витоки пам'яті в Three.js тихо катастрофічні

Об'єкти JavaScript автоматично збирає збирач сміття. Ресурси WebGL — ні. Коли ви створюєте BufferGeometry, Material чи Texture в Three.js, бібліотека завантажує дані на GPU. Збирач сміття JavaScript не може дотягнутися до драйвера GPU, щоб звільнити ці виділення — це робить лише явний виклик .dispose().

На сторінці, яка створює й знищує сцени (як наша, де користувачі перемикаються між 350 симуляціями), один забутий виклик dispose() накопичується в сотні осиротілих виділень GPU протягом кількох хвилин, зрештою спричиняючи чорні canvas-и, помилки втрати контексту та збої вкладок.

Патерн dispose()

function disposeScene(scene, renderer) { scene.traverse((obj) => { if (!obj.isMesh) return; // 1. Геометрія — VBO, IBO на GPU obj.geometry?.dispose(); // 2. Матеріал(и) — шейдерні програми, uniform-и const mats = Array.isArray(obj.material) ? obj.material : [obj.material]; for (const mat of mats) { // 3. Текстури, прив'язані до матеріалу for (const key of Object.keys(mat)) { if (mat[key]?.isTexture) mat[key].dispose(); } mat.dispose(); } }); // 4. Цілі рендерингу (фреймбуфери + текстури) renderer.renderLists.dispose(); }

Викликайте disposeScene() перед видаленням canvas з DOM і перед викликом renderer.dispose(). Порядок важливий: звільнення матеріалу запускає видалення шейдерної програми; звільнення рендерера руйнує контекст.

Ліміти WebGL-контексту

Chrome і Firefox накладають жорсткий ліміт на кількість WebGL-контекстів на сторінці (зазвичай 8 у Chrome, 16 у Firefox станом на 2026 рік). Кожен new THREE.WebGLRenderer() споживає один контекст. Перевищення ліміту тихо вбиває найстаріший контекст — canvas найстарішої вашої симуляції стає чорним.

Повторне використання контексту

Діліть один рендерер між симуляціями, змінюючи розмір його canvas замість створення нового рендерера при кожному циклі монтування/демонтування.

Явне звільнення

Викликайте renderer.dispose() та renderer.forceContextLoss(), коли симуляція справді вивантажується, а потім обнуляйте посилання.

Відновлення контексту

Слухайте canvas.addEventListener('webglcontextlost') та webglcontextrestored — відновлюйте геометрію та текстури при відновленні.

Текстурні атласи

Пакуйте кілька дрібних текстур в один атлас. Кожен об'єкт Texture — окреме завантаження на GPU; атласи зменшують і перемикання стану контексту, і фрагментацію VRAM.

Виявлення витоків за допомогою renderer.info

renderer.info відображає лічильники живих виділень GPU у реальному часі — найшвидший спосіб підтвердити, що dispose працює.

// Логуємо до і після dispose console.log('До:', { geometries: renderer.info.memory.geometries, textures: renderer.info.memory.textures, programs: renderer.info.programs?.length, }); disposeScene(scene, renderer); console.log('Після:', { geometries: renderer.info.memory.geometries, textures: renderer.info.memory.textures, programs: renderer.info.programs?.length, }); // Очікується: усі лічильники падають до 0 (або кількості спільних ресурсів)

performance.memory для відстеження купи

У браузерах на Chromium performance.memory надає usedJSHeapSize та totalJSHeapSize. Хоча це відстежує купу JS (не безпосередньо VRAM), великі дані текстур у ArrayBuffer справді з'являються тут ще до завантаження. Знімайте показники кожні 5 секунд у розробці, щоб вловлювати тенденції:

// Монітор купи лише для розробки if (import.meta.env.DEV && performance.memory) { setInterval(() => { const mb = (performance.memory.usedJSHeapSize / 1e6).toFixed(1); console.debug(`Купа: ${mb} МБ`); }, 5_000); }

Правило великого пальця: якщо ви це створили — ви це звільняєте. Three.js не володіє пам'яттю GPU — нею володіє ваш застосунок. Корисна ментальна модель: ставтеся до BufferGeometry, Material та Texture як до файлових дескрипторів. Ви завжди закриваєте файловий дескриптор, коли він більше не потрібен.