# Memory Leak Observed during webviewer model switching

**URL:** <https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920>\
**Category:** HOOPS Visualize Web\
**Tags:** code, sample, webviewer, learn\
**Created:** [September 26, 2025, 2:51pm UTC](https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920 "2025-09-26T14:51:39Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![raja.r](https://avatars.discourse-cdn.com/v4/letter/r/a3d4f5/32.png) [@raja.r](https://forum.techsoft3d.com/u/raja.r)\
**Post date:** [September 26, 2025, 2:51pm UTC](https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920/1 "2025-09-26T14:51:39Z")

</div>

**Issue:**  
Switching between models leads to increasing memory usage. The growth pattern differs between size of models, with bigger models severe leaks.

**Description:**

| Version | Initial Load | After Each Switch | After 2–3 Switches | Summary |
| --- | --- | --- | --- | --- |
| Model 1 (Small) | 400 MB | +150–200 MB | 1200–1500 MB | Memory grows steadily on each switch; never drops |
| Model 2 (Big) | 900 MB hike | +1500–1700 MB | 4000 MB | Severe memory leak observed |

**Observation:**

- Small Models shows moderate memory growth but remains manageable.

- Big models (not optimized ) exhibits a rapid and severe memory leak with each switch.

- but after viewer shutdown and switch to different model the increase memory is not reducing

---

<div class="post-metadata">

**Author:** ![tino](https://avatars.discourse-cdn.com/v4/letter/t/9dc877/32.png) [@tino](https://forum.techsoft3d.com/u/tino)\
**Post date:** [September 26, 2025, 3:13pm UTC](https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920/2 "2025-09-26T15:13:29Z")

</div>

Hello @raja.r,

What are the specific functions that you are calling to switch models?

Thanks,  
Tino

---

<div class="post-metadata">

**Author:** ![raja.r](https://avatars.discourse-cdn.com/v4/letter/r/a3d4f5/32.png) [@raja.r](https://forum.techsoft3d.com/u/raja.r)\
**Post date:** [September 26, 2025, 3:20pm UTC](https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920/3 "2025-09-26T15:20:12Z")

</div>

```auto
import EvmUtils from "./common/evmUtils.js";

export const _disposeViewer = async (viewer) => {
    if (!viewer) return;

    try {
        if (viewer.waitForIdle) {
            await viewer.waitForIdle();
        }
        if (viewer.shutdown) {
            await viewer.shutdown();
            await viewer.waitForIdle();
        }

        viewer = null;
        console.log("Viewer disposed successfully.");
    } catch (err) {
        console.error("Error while disposing viewer:", err);
    }
};

const _cleanupAllCanvases = (iafViewer) => {
    try {
        const widgetIds = Object.values({
            View3d: EvmUtils.EVMMode.View3d,
            View2d: EvmUtils.EVMMode.View2d,
        })

        widgetIds.forEach((widgetId) => {
            const container = iafViewer.evmElementIdManager.getEvmElementById(widgetId);
            if (!container) return;

            const canvases = container.querySelectorAll("canvas");
            canvases.forEach((canvas) => {
                // Clear 2D context
                const ctx = canvas.getContext("2d");
                if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);

                // Reset dimensions
                canvas.width = 0;
                canvas.height = 0;
            });
        });
    } catch (error) {
        console.error("Error in canvas cleanup:", error);
    }
};

export const cleanupAllResources = async (iafViewer) => {
    console.log('IafViewerDBM.cleanupAllResources', 'Starting comprehensive cleanup');

    try {
        if (iafViewer) {

            await _disposeViewer(iafViewer, iafViewer._viewer)
            await _disposeViewer(iafViewer, iafViewer._viewer2d)
        }

        _cleanupAllCanvases(iafViewer);

        if (typeof window !== 'undefined' && window.gc) {
            try {
                window.gc();
            } catch (error) {
                // GC not available or failed
            }
        }

        console.log('IafViewerDBM.cleanupAllResources', 'Cleanup completed successfully');

    } catch (error) {
        console.error('IafViewerDBM.cleanupAllResources', 'Error during cleanup:', error);
    }
}

```

---

<div class="post-metadata">

**Author:** ![tino](https://avatars.discourse-cdn.com/v4/letter/t/9dc877/32.png) [@tino](https://forum.techsoft3d.com/u/tino)\
**Post date:** [September 26, 2025, 3:35pm UTC](https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920/4 "2025-09-26T15:35:26Z")

</div>

The code you provided is doing the clean-up work. But how are you loading subsequent models? For example:

```auto
await hwv.model.clear();

await hwv.model.loadSubtreeFromScsFile(
	hwv.model.getAbsoluteRootNode(),
	"microengine.scs",
	optional_matrix
);

```

Also, which type of Stream Cache files are you loading SC/SCZ or SCS?

---

<div class="post-metadata">

**Author:** ![raja.r](https://avatars.discourse-cdn.com/v4/letter/r/a3d4f5/32.png) [@raja.r](https://forum.techsoft3d.com/u/raja.r)\
**Post date:** [September 26, 2025, 4:12pm UTC](https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920/5 "2025-09-26T16:12:54Z")

</div>

```auto
  _createViewer() {
    console.log ('Iaf3DViewer _createViewer');

    if (!this.props.view3d.enable) return;
    const { fileSet, model, authToken, wsUri, settings } = this.props;
    let viewerPromise;
    let streamCutoffScale = 0.5;
    this._viewerInitialized = true;
    if (settings && settings.hasOwnProperty("streamCutoffScale"))
      streamCutoffScale = parseFloat(settings.streamCutoffScale);

    // this.props.openNotification ("The model is being loaded (" + this.props.graphicsResources.views[0].title + ")");
    NotificationStore.notifyModelIsBeingLoaded(this, 0);

    const perfLogger = new IafPerfLogger("viewer.defaultView3d");

    // RRP:- Duplicate as it is called from modelStructure ready.
    // this.initalizeModelComposer();

    viewerPromise = this._hwvManager.createRemoteViewer(
      this.evmElementIdManager.getEvmUuid(EvmUtils.EVMMode.View3d),
      model,
      fileSet,
      authToken,
      wsUri,
      streamCutoffScale,
      0
    );

    viewerPromise.then((viewer) => {
      this._viewer = viewer;
      this._boundingBoxMarkup = new IafBoundingBoxMarkup(this._viewer);
      this.graphicsResources = this.props.graphicsResources;
      this._viewer.graphicsResources = this.graphicsResources;
      this.graphicsResources.setviewer(this, this._viewer);
      this._isolateZoomHelper = new ViewerIsolateZoomHelper(viewer, this);

      // Once the viewer is instantiated, we can set the state to true to have the React update the DOM
      this.setState({
        hwvInstantiated: true,
      });

      //use our own Select Operator

      this.selectOperator = new SelectOperator(
        this._viewer,
        this._viewer.noteTextManager,
        this
      );
      this.selectOperatorId = this._viewer.registerCustomOperator(
        this.selectOperator
      );
      this.iafCuttingPlanesUtils = new IafCuttingPlanesUtils(this._viewer
                                                        , this.newToolbarElement ? this.newToolbarElement.current : undefined);

      this._viewer.operatorManager.set(this.selectOperatorId, 1);
      this.measurementManager = new IafMeasurementManager(this._viewer,"Measurement3d",this.projectId);
      // Storing the callback in its own function to avoid registering a bound callback
      // (more difficult to unregister that in HC)
      //let parent = this;
      this._viewer.setCallbacks({
        // Add new callbacks at the top, so that they don't accidentally replace the earlier ones
        // 10-12-23 ATK MK-149 Added viewer callbacks for error handling
        modelStructureHeaderParsed: async (fileName, fileType) => {
          console.log ('viewer.callbacks.modelStructureHeaderParsed'
            , '/fileName', fileName
            , '/fileType', fileType
          );
        },
        missingModel: async (modelPath) => {
          console.log ('viewer.callbacks.missingModel'
            , '/modelPath', modelPath
          );
        },
        modelLoadBegin: async () => {
          console.log ('viewer.callbacks.modelLoadBegin'
          );
        },
        modelLoadFailure: async (modelName, reason, error) => {
          // TOBE: Reviewed for performace projects.
          NotificationStore.notifyModelIsMissing(this, modelName, true);
          console.error('viewer.callbacks.modelLoadFailure'
            , '/modelName', modelName
            , '/reason', reason
            , '/error', error
          );
        },
        modelLoaded: async (modelRootIds, source) => {
          console.log ('viewer.callbacks.modelLoaded'
            , '/modelRootIds', modelRootIds
            , '/source', source
          );
        },
        modelStructureLoadBegin: async () => {
          console.log ('viewer.callbacks.modelStructureLoadBegin'
          );
        },
        modelStructureLoadEnd: async () => {
          console.log ('viewer.callbacks.modelStructureLoadEnd'
          );
        },
        modelStructureParseBegin: async () => {
          console.log ('viewer.callbacks.modelStructureParseBegin'
          );
        },
        modelSwitchStart: async (clearOnly) => {
          console.log ('viewer.callbacks.modelSwitchStart'
            , '/clearOnly', clearOnly
          );
        },
        modelSwitched: async (clearOnly, modelRootIds) => {
          console.log ('viewer.callbacks.modelSwitched'
            , '/clearOnly', clearOnly
            , '/modelRootIds', modelRootIds
          );
        },
        viewLoaded: async (view) => {
          console.log ('viewer.callbacks.viewLoaded'
            , '/view', view
          );          
        },
        websocketConnectionClosed: async () => {
          console.log ('viewer.callbacks.websocketConnectionClosed'
          );                    
        },
        webGlContextLost: async () => {
          console.log ('viewer.callbacks.webGlContextLost'
          );                    
        },
        streamingActivated: () => {
          // if (!this.state.isModelStructureReady) this.setModelIsLoaded(this._viewer, false);
        },
        streamingDeactivated: async () => {
          // if (!this.state.isModelStructureReady) this.setModelIsLoaded(this._viewer, true);
        },
        addCuttingSection: async (cuttingSection /* : Communicator.CuttingSection */) => {
          console.log ('viewer.Callbacks.addCuttingSection', '/cuttingSection', cuttingSection);
        },
        removeCuttingSection: async () => {
          console.log ('viewer.Callbacks.removeCuttingSection');
        },
        cuttingPlaneDragStart: async (cuttingSection /* : Communicator.CuttingSection */,
                                      planeIndex /* : number */) => {
          console.log ('viewer.Callbacks.cuttingPlaneDragStart', 
                        '/cuttingSection', cuttingSection,
                        '/planeIndex', cuttingSection.planeIndex);
        },
        cuttingPlaneDragEnd: async (cuttingSection /* : Communicator.CuttingSection */,
                                    planeIndex /* : number */) => {
          // console.log ('viewer.Callbacks.cuttingPlaneDragEnd', 
          // '/cuttingSection', cuttingSection,
          // '/cuttingSection.planeIndex.logical', cuttingSection.planeIndex.logical,
          // '/planeIndex', cuttingSection.planeIndex,
          // '/d', cuttingSection.getPlane(0).d);
          this.iafCuttingPlanesUtils.onDragCuttingPlaneFromGraphics(cuttingSection);

        },
        cuttingPlaneDrag: async (cuttingSection /* : Communicator.CuttingSection */,
                                  planeIndex /* : number */) => {
          // console.log ('viewer.Callbacks.cuttingPlaneDrag', 
          // '/cuttingSection', cuttingSection,
          // '/cuttingSection.planeIndex.logical', cuttingSection.planeIndex.logical,
          // '/planeIndex', cuttingSection.planeIndex,
          // '/d', cuttingSection.getPlane(0).d
          // );
          this.iafCuttingPlanesUtils.onDragCuttingPlaneFromGraphics(cuttingSection);
        },
        // ATK: To Do: Get rid of the following as it appears redundant
        // sceneReady: () => {
        // const camera = this._viewer.view.getCamera();
        // // set saved intial camera position
        // if (this.props.settings.initialCameraPosition) {
        // let newCamera = window.Communicator.Camera.fromJson(
        // this.props.settings.initialCameraPosition
        // );
        // this._viewer.view.setCamera(newCamera);
        // }
        // },
        subtreeLoaded: (modelRootIds, source) => console.log('subtreeLoaded', source, modelRootIds),
        subtreeDeleted: (modelRootIds) => console.log('subtreeDeleted', modelRootIds),
        modelStructureReady: async () => {
          perfLogger.end();
          iafCallbackModelStructureReady(this);
        },
        measurementValueSet: async (m) => {
          const { multiplier } = this.state;
          let mValue = m.getMeasurementValue();
          let uMultiplier = m.getUnitMultiplier();
          let mmUnits = mValue * uMultiplier;
          let newUnitMul = multiplier;
          m.setUnitMultiplier(newUnitMul);

          m._measurementValue = mmUnits / newUnitMul;
          m.setMeasurementText(
            Communicator.Util.formatWithUnit(m._measurementValue, newUnitMul)
          );
        },

        selectionArray: async (selectionEvents) => {
          if (selectionEvents.length === 0) return;

          //selectionEvents only represents the latest select event
          //use viewer.selectionManager.getResults to get current selected nodes
          // or selectionEvents[0]._selection._nodeId

          let id = this.getActiveSelectionNodeId(this._viewer, selectionEvents);
          let elementId = id ? this.getClosestElementIds([id]) : undefined;
          // let csdlOffset = this._viewer.model.getNodeIdOffset(id);

          if (!id || !_.size(elementId)) {
            console.log ('Viewer.selectionArray.3D'
              , 'Invalid callback'
              , '!id || !_.size(elementId)'
              , '/id', JSON.stringify(id)
              // , '/csdlOffset', csdlOffset
              , '/elementId', JSON.stringify(elementId)
            );
            // HSK PLAT-4891: Bug - 3D Text Markup is editable only on creation
            // this.markupManager && this.markupManager.drawTextOperator.selectTextboxItem(id);
            NotificationStore.notifyNoBimAssociationFound(this);
            return;
          }
        
          console.log ('Viewer.selectionArray.3D'
            , '/id', id
            , '/this.prevSelection', JSON.stringify(this.prevSelection)
            , '/elementId', JSON.stringify(elementId)
          );
        
          return this.handleElementGraphicsSelection(elementId, false);
        },
        sceneReady: async () => {
          console.log ('viewer.callbacks.sceneReady');
          let camPos, target, upVec;
          /*camPos = new window.Communicator.Point3(0, 200, 400);
          target = new window.Communicator.Point3(0, 1, 0);
          upVec = new window.Communicator.Point3(0.2, 1, 0.5);
          const defaultCam = window.Communicator.Camera.create(
            camPos, target, upVec, 1, 720, 720, 0.01);
          this._viewer.view.setCamera(defaultCam);*/

          // Background color for viewers
          this._viewer.view.setBackgroundColor(
            new window.Communicator.Color(252, 252, 252),
            new window.Communicator.Color(230, 230, 230)
          );

          //check
          // if opacity works
          this.setNodeSelectionColor(this._viewer, bdSelectColor);
          this.setNodeSelectionOutlineColor(this._viewer, bdSelectColor);
          this.setNodeElementSelectionColor(this._viewer, faceSelectColor);
          this.setNodeElementSelectionOutlineColor(this._viewer, lineSelectColor);

          //PLAT-575: turn off parent selecting for now as it's not a helpful feature now
          this._viewer.selectionManager.setSelectParentIfSelected(false);
          //Default draw mode to Shaded rather than WireframeOnShaded
          this._viewer.view.setDrawMode(Communicator.DrawMode.Shaded);
          //if silhouetted edges are enabled, be sure to disable them in glass mode
          this._viewer.view.setSilhouetteEnabled(true);
          // set ambient occlusion mode and radius
          this._viewer.view.setAmbientOcclusionEnabled(true);
          this._viewer.view.setAmbientOcclusionRadius(0.01);
          //this._viewer.view.setBloomEnabled(true)
          //this._viewer.view.setBloomIntensityScale(0.1)
          
          try {
            // Calling initializeOperators to set up navigation operators
            await this.initializeOperators();
            console.log('Navigation operators successfully initialized.');
          } catch (error) {
            console.error('Error initializing navigation operators:', error.message);
          }
          
          // if(this._drawMode === IafDrawMode.Glass)
          // this.setXrayModeSettings()
          
          // object of IafSavedViews class
          this.savedViewsManager = new IafSavedViews();
          // Create Navigation cube
          if(this.props.isShowNavCube || typeof this.props.isShowNavCube === "undefined"){
            await this._viewer.view.getNavCube().enable();
          await this._viewer.view
            .getNavCube()
            .setAnchor(window.Communicator.OverlayAnchor.LowerRightCorner);
          }
          if (
            this.isElementThemingIdExists(this.props.colorGroups) ||
            this.props?.sliceElementIds?.length > 0
          )
            this.forceUpdateViewerElements = true;

          this.isSceneReady = true;
          console.log("Web Viewer has been initialized.");
        },
        timeout: () => {
          console.log ('viewer.callbacks.timeout');
          if (
            window.confirm(
              "This page timed out due to inactivity. Press OK to refresh."
            )
          ) {
            location.reload();
          }
        },
        camera: (camera) => {
          iafCallbackCamera(this, camera);
        },
        // HSK PLAT-4861: UX - Move markup options to under Measurements (Annotations)
        measurementCreated: (measurement) => {
          !this.markupManager.repeatLastMode && this._viewer.operatorManager.set(this.selectOperatorId, IafOperatorUtils.IafOperatorPosition.Operation);
        }
      });
      // window.addEventListener("resize", this.onResize);
      // this.evmElementIdManager.getEvmElementById(EvmUtils.EVMMode.View3d).addEventListener("contextmenu", (e) => contextmenu(e, this));
      // added click listner to close active submenu when clicked on viewer
      // this.evmElementIdManager.getEvmElementById(EvmUtils.EVMMode.View3d).addEventListener("mousedown", (e) => mousedown(this));
      // document.addEventListener("click", (e) => {this.contextMenu.style.display = "none";})
    });
  }
    createRemoteViewer(containerId, model, fileSet , authToken, wsUri, streamCutoffScale, defaultViewIndex) {
    let uri = wsUri + '/graphicssvc?fileSetId=' + fileSet._id +
      '&nsfilter=' + model._namespaces[0] +
      '&token=' + authToken + '&serverVersion=4'

    // let fileName = fileSet && _.size(fileSet._files) > 0 ? fileSet._files[0]._fileName : model._name + '.scz'
    let fileName = IafUtils.buildFileName(model, fileSet, defaultViewIndex ? defaultViewIndex : 0);

    let viewerPromise = new Promise((resolve) => {
      let params = {
        containerId: containerId,
        streamMode: window.Communicator.StreamingMode.All,
        rendererType: window.Communicator.RendererType.Client,
        streamCutoffScale: streamCutoffScale,
        //memoryLimit: 512,
        //empty: true
        endpointUri: uri,
        boundingPreviewMode: Communicator.BoundingPreviewMode.None,
        // model: window.Communicator.EmptyModelName//model._name + '.scz'
        model: fileName//model._name + '.scz'
      };
      console.log ('ViewerManager.createRemoteViewer', params);

      // PLG-1263: EVM 1.0 - Multiple instances of viewer - Hack to wait for div to be created
      IafUtils.waitForElement(containerId).then ((container) => {
        let _viewer = new window.Communicator.WebViewer(params);
        _viewer.start();
        resolve(_viewer);
      })
    })
      
    return viewerPromise;
  }

```

2023\_SP1 usign hoops communicator

---

<div class="post-metadata">

**Author:** ![tino](https://avatars.discourse-cdn.com/v4/letter/t/9dc877/32.png) [@tino](https://forum.techsoft3d.com/u/tino)\
**Post date:** [September 26, 2025, 6:20pm UTC](https://forum.techsoft3d.com/t/memory-leak-observed-during-webviewer-model-switching/4920/6 "2025-09-26T18:20:51Z")

</div>

A few things to note:

- You are using an older version of Communicator (HC 2023 SP1) that is more than two full generations behind (current version is HC 2025.6.1). There have been server improvements and bug fixes between these releases.
- You can try calling `curl -X GET http://localhost:11182/api/spawns` to confirm that the number of spawns is equal to the active viewer sessions. NOTE: replace “localhost” with your hostname.
- It’s unclear why you are calling `waitForIdle()` after shutting down the viewer. Please see [more information in our docs about this function](https://docs.techsoft3d.com/communicator/latest/prog_guide/viewing/data_model/stream_cache/loading.html#determine-if-a-model-is-fully-loaded). It is used to check if the model has fully loaded in the viewer (which is a moot point if it was shut down).
- Let’s try deleting the viewer after shutdown:

```auto
this._viewer.shutdown();
delete this._viewer;

```
