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ć.