visgl / react-google-maps

React components and hooks for the Google Maps JavaScript API
https://visgl.github.io/react-google-maps/
MIT License
1.26k stars 104 forks source link

[Feat] Improve Performance when Props of a `Map` with Lots of `Marker`s are Controlled #442

Closed anliting closed 4 months ago

anliting commented 4 months ago

Target Use Case

Optimize performance.

Proposal

This is an example about a Map with lots of Markers:

import React,{useState}from         'react'
import{APIProvider,Map,Marker}from  '@vis.gl/react-google-maps'
export default()=>{
    return<APIProvider apiKey=''>
        <Map
        >{[...Array(1000)].map(()=>
            <Marker position={{lat:0,lng:0}} />
        )}</Map>
    </APIProvider>
}

If we open the webpage with performance recording, continuously pan the map, we get some profiles like this:

image

But when we controll its props:

  import React,{useState}from         'react'
  import{APIProvider,Map,Marker}from  '@vis.gl/react-google-maps'
  export default()=>{
+     let[cameraProps,setCameraProps]=useState({})
      return<APIProvider apiKey=''>
          <Map
+             {...cameraProps}
+             onCameraChanged={e=>setCameraProps(e.detail)}
          >{[...Array(1000)].map(()=>
              <Marker position={{lat:0,lng:0}} />
          )}</Map>
      </APIProvider>
  }

We get some profiles like this:

image

In my intuition, these two examples would have roughly the same performance in imperative programming. But I am not sure if such an optimization in React is easy or not.

I would be grateful if it is optimized. Thank you in advance.

usefulthink commented 4 months ago

The main problem here is this: by specifying the position with position={{lat:0, lng:0}} you are creating new objects with every render of the main component. Since React can't know what you meant to do, it will just see a different object being passed to the position-prop and re-render the Marker component (at least that would be the case once you added the key props), which then has to update the position (although the position didn't actually change). This is the part that is expensive here. If you memoize the markers for example, you won't have any such problems.

There are of course a few smaller optimizations we can add to the Marker component, but Applications still need to do their part avoiding unnecessary re-renders.

anliting commented 3 months ago

I also tried to

  import React,{useState}from         'react'
  import{APIProvider,Map,Marker}from  '@vis.gl/react-google-maps'
+ let MemoMarker=React.memo(Marker)
  export default()=>{
      let[cameraProps,setCameraProps]=useState({})
+     let onCameraChanged=React.useCallback(e=>setCameraProps(e.detail))
+     let marker=React.useMemo(()=>[...Array(1000)].map((e,i)=>
+         <MemoMarker key={i} position={{lat:0,lng:0}} />
+     ),[])
    return<APIProvider apiKey=''>
        <Map
            {...cameraProps}
-           onCameraChanged={e=>setCameraProps(e.detail)}
+           onCameraChanged={onCameraChanged}
-         >{[...Array(1000)].map(()=>
-             <Marker position={{lat:0,lng:0}} />
-         )}</Map>
+       >{marker}</Map>
    </APIProvider>
}

image

usefulthink commented 3 months ago

Interesting, that is pretty much spot on, and in that case the marker-components shouldn't have to re-render at all even if the map-props are changed. I re-created that in codesandbox here: https://codesandbox.io/p/devbox/snowy-dream-hnwmyh

You can see in this example that the performance problems are coming from the google maps API itself and there's nothing we can do about that. With 1000 markers, updating the DOM for the markers seems to be the main problem. It's getting a lot better when switching to AdvancedMarker instead, but those will have that kind of performance ceiling as well.