Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Implement Static fallback via the Maps Static API #480

Open
housseindjirdeh opened this issue Aug 12, 2024 · 5 comments
Open

[Feat] Implement Static fallback via the Maps Static API #480

housseindjirdeh opened this issue Aug 12, 2024 · 5 comments

Comments

@housseindjirdeh
Copy link

housseindjirdeh commented Aug 12, 2024

Target Use Case

Minimize the user experience impact of the library by improving paint time and responsiveness metrics.

Proposal

Data from HTTP Archive shows that Google Maps has the longest download times during page rendering when compared to the other most popular third-party libraries on the web:

Median download time during page rendering for the top 10 most popular third parties (calculated using HTTP Archive and Lighthouse’s wastedMs attribute via the render blocking audit)
Query

This happens because the Maps JavaScript API fetches ~240 kB of JavaScript across multiple scripts. In this library, we can circumvent this by delaying the instantiation of these scripts until the user engages with the map. While waiting for the user’s interaction, we can fetch and display an image via the Maps Static API passing in the same set of required props (center and zoom).

Optimized prototype of react-google-maps that displays an image and defers JS until user interaction)
Link

To implement this feature, the <APIProvider> component of the library can keep track of whether the map has been selected as part of the context information it provides. We can use this to conditionally render either the “real” map versus a static version of it:

// src/components/map/index.tsx

import { StaticMap } from "./static-map";
// ...

return (
  <div onClick={() => context.setMapSelected(true)}>
    >
    {context.mapSelected ? (
      // Load normal map
    ) : (
      <StaticMap {...props} /> // Load Static Map if map hasn't been clicked
    )}
  </div>
);

You can see all the changes in this prototype in this commit. Note that this is just a prototype, and the real implementation would offer some sort of UI that makes it clear that the map needs to be clicked in order to be interacted with.

Performance Tests

Comparing the current version of react-google-maps with the prototype above on the same site shows a significant difference in rendering times:

When the user interacts with the map, the same scripts are still fetched and executed. But by deferring them until they’re needed, the browser's main thread is freed up to handle other tasks while the document continues to load. This will improve both Largest Contentful Paint and Interaction to Next Paint.

The chart and table below present a comparison of these two metrics when applying this optimization to a representative Next.js website: https://news-site-next-static.netlify.app/.

  Largest Contentful Paint Total Blocking Time*
News Site Next 3.38s 0.25s
News Site Next with react-google-maps 7.12s (+3.74s) 0.97s  (+0.72s)
News Site Next with react-google-maps (Optimized) 4.22s (+0.84s) 0.44s (+0.19s)

Test conditions: WebPageTest - Emulated 4G Connection, Chrome, Moto G4 (Median Results - 3 Runs)
*Total Blocking Time is used as a lab proxy here for Interaction to Next Paint

@housseindjirdeh housseindjirdeh changed the title [Feat] Static fallback [Feat] Implement Static fallback via the Maps Static API Aug 12, 2024
@mrMetalWood
Copy link
Collaborator

Hi @housseindjirdeh,

wow 😍 what a detailed and thought out issue and proposal!

I could totally see to have this as an option integrated somehow. @usefulthink will be back in a few weeks and I'm sure he will take a good look at this.

One thing that comes to mind immediately when it dealing with the Static Maps API are the size limitations. 640x640 which can be scaled up to 1280x1280 (https://developers.google.com/maps/documentation/maps-static/start#Imagesizes)
But I assume the target use case would not be a big fullscreen map anyway.

@housseindjirdeh
Copy link
Author

Thanks @mrMetalWood, I appreciate the kind words. Looking forward to hearing what @usefulthink thinks when they get the chance to take a look.

One thing that comes to mind immediately when it dealing with the Static Maps API are the size limitations. 640x640 which can be scaled up to 1280x1280 (https://developers.google.com/maps/documentation/maps-static/start#Imagesizes)

Yeah absolutely, this is a risk that we may have to be aware of. My assumption is that most Maps don't exceed 1280x1280 on the web but we may want to handle cases where they are (e.g. clear docs, console warning?)

@housseindjirdeh
Copy link
Author

Some other concerns we'll have to think about:

  1. Billing: The Maps Static and JavaScript APIs both use separate pay-as-you-go pricing models (0.002 and 0.007 USD per each map respectively). I wish I had data to validate this, but I'm hoping that this would help lower costs with the assumption that the percentage of users that generally interact with a map is lower than the anticipated cost increase. Regardless, we'll likely have to mention this in the documentation somewhere and provide users with a way to opt-out and only load a dynamic map. Something like:

    const App = () => (
      <APIProvider apiKey={process.env.GOOGLE_MAPS_API_KEY}>
        <Map
          // ...
          dynamicOnly // Optional prop to only load the dynamic map
        />
      </APIProvider>
    );
    ``
  2. Slight differences in rendered image: Even with the same coordinates and zoom levels applied, the rendered image from the Static and JavaScript APIs do not match exactly.

    This may not be a significant concern but we should still try to minimize the discrepancy between the two rendered images as much as possible.

@usefulthink
Copy link
Collaborator

Awesome work, @housseindjirdeh – thanks for the detailed analysis and suggestions, I like the ideas a lot.

As for the cost, A quick calculation shows that having the static initial view would be cheaper when less than ~71% of users activate the dynamic map (solving r * (2+7) + (1-r) * 2 = 7 for the ratio of dynamic map loads r gives r=0.714..., r * (2+7) being the cost for dynamic + static map shown, and (1-r) * 2 would be the static map only in the remaining cases).

The problem I'm seeing with this: A simple implementation would only cover rendering of the basic map, and nothing on top of it. This would also mean the dynamic map would be required most of the time just for rendering what is supposed to be rendered (markers etc). The only exception here would be cases where the map doesn't even enter the viewport and/or isn't relevant to the user.

We could possibly implement support for markers, polygons and polylines in a limited way, although I'm not yet sure how that should look API wise. Supporting the marker components we already have would be difficult, since there's only a limited feature-set supported by the static maps API (the Marker component might partially work, the AdvancedMarker probably won't).

With markers etc supported, we could actually reach a point where a lot of simpler use-cases can be supported using the static map as well – making the static preview financially viable as a default.

Then there's the point of the maximum size you also mentioned, which limits static maps to 640x640px CSS size unless a special project specific agreement is made that could allow up to 2048px squared. For most mobile uses this should be more than enough, and probably also where the performance impact would be the most significant. Maybe the Maps Platform Team at Google has some data on real-world usage of maps and some numbers on usual sizes. That would tell us in how many instances a static fallback would actually work. I'll ask them when I get the next opportunity.

To summarize, here's what I think would be some good steps to move this forward:

  • implement the StaticMap component that also supports SSR in next.js and other frameworks (i.e. statically renders as an image tag with signed url)

  • implement an example demonstrating switching from a static to a dynamic map (essentially what you've already done, but in a way that doesn't tie the switching directly into the Map component). This would also be helpful for developers seeking to avoid activating the map when it's not actually needed, or want to provide their own fallback content to show before the map is loaded.

  • from that example, we can extract a hook or component to encapsulate the logic for switching between static preview and dynamic map, to make it as easy as possible for users to add this behavior to their map. <MapWithStaticFallback> is a terrible name to suggest, but you get the idea.

  • implement a StaticMapMarker component (or figure out a way to integrate it with existing marker components, but I think we can figure out a way to reconcile this with the Marker component later). Just as the StaticMap, this has to work in SSR environments as well.

I think when we have all those things in place we can revisit the question of integrating it into the Map component itself and possibly making it the default behavior.

What do you think, would you be able to take on some of these tasks or maybe even take the lead for this?

@housseindjirdeh
Copy link
Author

Thanks for taking a look @usefulthink. Really appreciate the detailed feedback.

We could possibly implement support for markers, polygons and polylines in a limited way,

Agreed. I would definitely want to support markers and additional features as much as possible but also understand that we may not cover 100% of functionality. Ideally we would be able to get close enough and then be clear in the documentation that certain things may not be supported as expected.

To summarize, here's what I think would be some good steps to move this forward:

All those steps sound good to me. I would be more than happy to take the lead on this and I'll reach out whenever I have questions. I can start by submitting a base level StaticMap component and we can go from there :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants