jsapi-resources icon indicating copy to clipboard operation
jsapi-resources copied to clipboard

Web workers' memory usage increases when remounting the map (react)

Open TimurGolovinov opened this issue 3 years ago • 3 comments

IMPORTANT

  • [X] My question is related to the samples and content within this repository. For all other issues, open a ticket with Esri Technical Support or post your question in the community forum or .
  • [X] I have checked for existing issues to avoid duplicates. If someone has already opened an issue for what you are experiencing, please add a 👍 reaction and comment as necessary to the existing issue instead of creating a new one.

Actual behavior

We use ArcGIS JS 4.21.1 library in our single-page react application. The map can be opened and closed multiple times during a user session. Every time the map is being opened it creates web workers, however, when the map is being closed (the component is unmounted) the web workers are not being cleared. Memory usage starts growing with every subsequent map opening.

We measure this through Chrome Inspector -> Memory. Memory usage grows significantly higher when having layers, graphics, directions, etc... ).

Expected behavior

When the map remounts, the memory usage of web workers is not increasing

Reproduction sample

https://github.com/TimurGolovinov/react-arcgis-remount-test

Reproduction steps

Steps to reproduce:

  1. Install dependencies - npm ci
  2. Run the app - npm start
  3. When the app is running go to the browser (Chrome) -> right click -> Inspect -> Memory
  4. Click the "Remount" button multiple times at 5-10 seconds intervals (wait for basemap to load), and you can see how memory consumption in esri-worker goes up, the more you click.

Reproduction browser

Chrome Version 102.0.5005.61 (Official Build) (x86_64)

Operating System (check https://whatsmyos.com)

-OS: macOS Monterey 12.5

Device: MacBook Pro (Retina, 15-inch, Mid 2015)

(same happens on windows machines as well)

TimurGolovinov avatar Sep 02 '22 07:09 TimurGolovinov

Tested this out in Chrome 104 and the memory usage isn't drastic, goes from ~90 to ~110, then drops down back to ~90 after a few clicks. However, the main issue you are seeing is re-instantiating the Map and View between mounts. The recommended pattern if your application is going to mount/remount the View is to preserve the view and just remove the container.

import "./App.css";
import { useLayoutEffect, useState, useCallback, useRef } from "react";
import Map from "@arcgis/core/Map";
import MapView from "@arcgis/core/views/MapView";

export default function App() {
  const [mapInitialised, setMapInitialised] = useState(false);
  const [mounted, setMounted] = useState(false);

  const containerRef = useRef(null);
  const mapRef = useRef(null);
  const viewRef = useRef(null);

  useLayoutEffect(() => {
    if (!mounted) {
      setMounted(true);
      return;
    }

    if (mapInitialised) return;

    console.log("App loaded");

    if (viewRef.current) {
      viewRef.current.container = containerRef.current;
    } else {
      const map = new Map({
        basemap: "topo-vector",
      });
      mapRef.current = map;

      const view = new MapView({
        map: map,
        center: [-118.805, 34.027],
        zoom: 13,
        container: containerRef.current,
      });

      viewRef.current = view;
    }

    setMounted(true);
    setMapInitialised(true);
  }, [mapInitialised, mounted]);

  const remount = useCallback(() => {
    setMounted(false);
    setMapInitialised(false);
  }, []);

  return (
    <div className="App">
      {mounted && (
        <div
          id="viewDiv"
          ref={containerRef}
          style={{ width: "100vw", height: "90vh" }}
        />
      )}
      <button onClick={() => remount()}>Remount</button>
    </div>
  );
}

odoe avatar Sep 02 '22 15:09 odoe

Thank you, @odoe, it is definitely a good optimisation for us.

Another alternative I was thinking about, is to terminate the workers once the map component is unmounted. In our case, we have a CPU-intensive app, which becomes problematic on mobile devices. Sometimes after intense map use, the app crashes, therefore we always try to minimise memory consumption.

The current behavior is that after the unmount, the workers are still running. The desired behavior would be to terminate workers after the component is unmounted.

Please let me know if this is possible to access the esri-workers to run worker.terminate() function? https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#terminating_a_worker

TimurGolovinov avatar Sep 06 '22 04:09 TimurGolovinov

Here's the code example for unmounting

  import "./App.css";
  import { useLayoutEffect, useState, useCallback, useRef } from "react";
  import Map from "@arcgis/core/Map";
  import MapView from "@arcgis/core/views/MapView";

  export default function App() {
    const [mapInitialised, setMapInitialised] = useState(false);
    const [mounted, setMounted] = useState(false);

    const [unmount, setUnmount] = useState(false);

    const containerRef = useRef(null);
    const mapRef = useRef(null);
    const viewRef = useRef(null);

    useLayoutEffect(() => {
      if (!mounted) {
        setMounted(true);
        return;
      }

      if (mapInitialised) return;

      console.log("App loaded");

      if (viewRef.current) {
        viewRef.current.container = containerRef.current;
      } else {
        const map = new Map({
          basemap: "topo-vector",
        });
        mapRef.current = map;

        const view = new MapView({
          map: map,
          center: [-118.805, 34.027],
          zoom: 13,
          container: containerRef.current,
        });

        viewRef.current = view;
      }

      setMounted(true);
      setMapInitialised(true);
    }, [mapInitialised, mounted]);

    const remount = useCallback(() => {
      setMounted(false);
      setMapInitialised(false);
    }, []);

    const unmountMap = useCallback(() => {
      setUnmount(true);
      //viewRef.current.destroy();
    // mapRef.current.destroy();
    }, []);

    return (
      <div className="App">
        {!unmount && mounted && (
          <div
            id="viewDiv"
            ref={containerRef}
            style={{ width: "100vw", height: "90vh" }}
          />
        )}
        <button onClick={() => remount()}>Remount</button>
        <button onClick={() => unmountMap()}>Unmount</button>
      </div>
    );
  }

TimurGolovinov avatar Sep 06 '22 05:09 TimurGolovinov

Closing, answer provided. Let us know if additional issues come up.

andygup avatar Mar 15 '23 19:03 andygup