Zarządzanie pamięcią w Three.js — dispose(), limity kontekstu WebGL i wykrywanie wycieków

Każda geometria, materiał i tekstura, które tworzysz w Three.js, zajmują pamięć GPU. Jeśli nigdy nie wywołasz dispose(), ta pamięć nigdy nie wraca — a przeglądarki ograniczają liczbę kontekstów WebGL na stronie do 8–16. Oto wszystko, czego nauczyliśmy się, zarządzając pamięcią w 350 interaktywnych symulacjach.

Dlaczego wycieki pamięci w Three.js są cicho katastrofalne

Obiekty JavaScript są automatycznie zbierane przez garbage collector. Zasoby WebGL — nie. Gdy tworzysz BufferGeometry, Material lub Texture w Three.js, biblioteka wysyła dane na GPU. Garbage collector JavaScriptu nie może sięgnąć do sterownika GPU, by zwolnić te alokacje — robi to wyłącznie jawne wywołanie .dispose().

Na stronie, która nieustannie tworzy i niszczy sceny (jak nasza, gdzie użytkownicy przełączają się między 350 symulacjami), jedno zapomniane wywołanie dispose() narasta w ciągu kilku minut do setek osieroconych alokacji GPU, co ostatecznie powoduje czarne płótna (canvas), błędy utraty kontekstu i awarie kart przeglądarki.

Wzorzec dispose()

function disposeScene(scene, renderer) { scene.traverse((obj) => { if (!obj.isMesh) return; // 1. Geometria — VBO, IBO na GPU obj.geometry?.dispose(); // 2. Materiał(y) — programy shaderów, uniformy const mats = Array.isArray(obj.material) ? obj.material : [obj.material]; for (const mat of mats) { // 3. Tekstury powiązane z materiałem for (const key of Object.keys(mat)) { if (mat[key]?.isTexture) mat[key].dispose(); } mat.dispose(); } }); // 4. Cele renderowania (framebuffery + tekstury) renderer.renderLists.dispose(); }

Wywołuj disposeScene() przed usunięciem płótna z DOM i przed wywołaniem renderer.dispose(). Kolejność ma znaczenie: zwolnienie materiału wyzwala usunięcie programu shadera; zwolnienie renderera burzy kontekst.

Limity kontekstu WebGL

Chrome i Firefox narzucają twardy limit liczby kontekstów WebGL na stronie (zazwyczaj 8 w Chrome, 16 w Firefoksie na rok 2026). Każdy new THREE.WebGLRenderer() zużywa jeden kontekst. Przekroczenie limitu po cichu zabija najstarszy kontekst — płótno twojej najstarszej symulacji staje się czarne.

Ponowne użycie kontekstu

Dziel jeden renderer między symulacjami, zmieniając rozmiar jego płótna zamiast tworzyć nowy renderer przy każdym cyklu montowania/odmontowania.

Jawne zwalnianie

Wywołuj renderer.dispose() i renderer.forceContextLoss(), gdy symulacja jest rzeczywiście wyładowywana, a następnie wyzeruj referencję.

Odzyskiwanie kontekstu

Nasłuchuj canvas.addEventListener('webglcontextlost') oraz webglcontextrestored — odbuduj geometrię i tekstury po odzyskaniu.

Atlasy tekstur

Pakuj wiele małych tekstur w jeden atlas. Każdy obiekt Texture to osobne przesłanie na GPU; atlasy redukują zarówno przełączanie stanu kontekstu, jak i fragmentację VRAM.

Wykrywanie wycieków za pomocą renderer.info

renderer.info udostępnia liczniki bieżących alokacji GPU na żywo — to najszybszy sposób, by potwierdzić, że dispose działa.

// Logujemy przed i po dispose console.log('Przed:', { geometries: renderer.info.memory.geometries, textures: renderer.info.memory.textures, programs: renderer.info.programs?.length, }); disposeScene(scene, renderer); console.log('Po:', { geometries: renderer.info.memory.geometries, textures: renderer.info.memory.textures, programs: renderer.info.programs?.length, }); // Oczekiwane: wszystkie liczniki spadają do 0 (lub liczby zasobów współdzielonych)

performance.memory do śledzenia sterty

W przeglądarkach opartych na Chromium performance.memory udostępnia usedJSHeapSize i totalJSHeapSize. Choć śledzi to stertę JS (nie bezpośrednio VRAM), duże dane tekstur w ArrayBuffer pojawiają się tu jeszcze przed przesłaniem. Próbkuj to co 5 sekund podczas developmentu, by wyłapywać trendy:

// Monitor sterty tylko na potrzeby developmentu if (import.meta.env.DEV && performance.memory) { setInterval(() => { const mb = (performance.memory.usedJSHeapSize / 1e6).toFixed(1); console.debug(`Sterta: ${mb} MB`); }, 5_000); }

Zasada kciuka: jeśli coś stworzyłeś, ty to zwalniasz. Three.js nie jest właścicielem pamięci GPU — jest nim twoja aplikacja. Przydatny model mentalny: traktuj BufferGeometry, Material i Texture jak uchwyty plików. Zawsze zamykasz uchwyt pliku, gdy skończysz z niego korzystać.