A texture leak often begins as an ownership disagreement. One component creates a resource, another caches it, and a third assumes removing a mesh will release everything associated with it.
The solution begins with a lifecycle you can describe before you choose a disposal technique.
Name the owner
For every resource, record its creator, current consumers, sharing policy, and retirement condition. A scene-local material can have a short lifetime. A texture shared by several scenes needs a different owner.
The Three.js cleanup guide explains that geometries, materials, and textures require explicit disposal. Removing an object from a scene does not by itself settle those responsibilities.
Do not solve every leak with a recursive traversal that disposes everything it encounters. A traversal sees references; it does not know whether another scene still owns one.
Distinguish the kinds of memory
JavaScript heap, decoded image data, GPU textures, geometry buffers, render targets, and renderer caches are different parts of the budget.
A small JPEG can decode into a much larger pixel array. A stable JavaScript heap does not establish stable GPU allocation. A renderer’s resource count is useful evidence, but it is not a universal byte counter for all device memory.
Use exact arithmetic where the format is known. A 4,096-square RGBA8 base level has 16,777,216 texels at four bytes each: 64 MiB. A full square mip chain adds approximately one third. The memory lab sums the actual levels.
Make stale loads harmless
Imagine a user selects material A, then B. If A finishes last, an unguarded loader may apply it after B or retain it in a cache no current scene needs.
Assign a generation or request identity to the load. When it completes, check whether its result still belongs to the current state. A superseded result must be released through its owner or placed in a deliberately bounded cache.
Cancellation is useful, but a request can finish just before cancellation arrives. The completion path still needs the identity check.
Avoid timer-based guarantees
The archived draft promoted fixed delays and a universal pause-dispose-resume sequence. Those may describe a workaround in a particular application, but they are not general proof that a resource is no longer referenced.
Follow the renderer’s current lifecycle and synchronization rules. Replacing an object, invalidating cached state, stopping future submissions, and waiting for already-submitted work are distinct concerns.
A delay that appears reliable on one machine can fail when frame scheduling, device speed, or workload changes.
Account for post-processing and loader ownership
The current Three.js post-processing guide notes that multiple-render-target attachments default to half-float RGBA unless configured otherwise. Those outputs can dominate a small product scene’s budget. Count every attachment at its actual size and sample count, alongside the textures visible in the material inspector. Post-processing and MRT.
A 1920 × 1080 single-sample RGBA16F color attachment contains about 15.82 MiB of pixel data. That calculation excludes depth, multisampling, mipmaps, alignment, and driver overhead. Four such attachments already imply about 63.28 MiB before the first product texture. Use arithmetic to find suspects, then measurements to judge the application.
The current KTX2Loader API also has a loader-level dispose() operation. Retiring a loader’s internal resources and retiring the textures it produced are separate ownership decisions. A surviving consumer must not inherit a disposed shared texture.
Use a retirement ledger
For each resource, log an identifier, owner, configuration generation, estimated bytes, consumer count, and retirement reason. Keep the log bounded. At the end of a repeated style-switch test, explain every surviving resource as active work or an intentional cache entry.
A useful failure exercise delays the first material load, completes a second selection, then lets the first finish. The first completion must fail its generation check and release its unclaimed result. Repeat while closing the viewer. Check for pending callbacks as well as allocated objects.
This ledger is an application method, not a new GPU-memory API. Its value is that it can identify an owner when process memory grows, instead of relying on a timer and hoping the old scene has disappeared.
Reproduce the growth
Define one cycle: open a model, change its material, switch styles, and return to the starting state. Repeat it while logging resource creation and retirement.
Look for a bounded plateau after caches warm. If retained resources grow on every cycle, inspect which owner still references them. Keep cache growth separate from resources that should have been retired.
Repeat after an interrupted load and an unmount. Those paths often receive less attention than the successful first render.
Report the limit honestly
Do not publish a “safe memory budget” for an entire phone family without current device evidence. Browser behavior, other applications, operating system state, and asset patterns affect what happens.
Instead, state the devices and workload tested, the allocations estimated, the observations collected, and the fallback triggered when the application struggles.
Memory discipline is not a single clever cleanup function. It is agreement about who may keep a resource and who must let it go.
Keep a good idea close.
Follow Signal Tower

