Чому витоки пам'яті в 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 як до файлових дескрипторів. Ви завжди
закриваєте файловий дескриптор, коли він більше не потрібен.