Skip to content

Lazy-load Google Maps to reduce billable Maps JavaScript API usage - #1470

Closed
lgarofalo wants to merge 1 commit into
ShelterTechSF:masterfrom
lgarofalo:lazy-load-google-maps
Closed

Lazy-load Google Maps to reduce billable Maps JavaScript API usage#1470
lgarofalo wants to merge 1 commit into
ShelterTechSF:masterfrom
lgarofalo:lazy-load-google-maps

Conversation

@lgarofalo

Copy link
Copy Markdown
Member

Problem

Both SearchMap and MapOfLocations mount a <GoogleMap> (via google-map-react, which loads the Maps JavaScript API) unconditionally on every render. Each mount is a billable Google Maps Platform "map load" event, and this happens for every visitor on every page view, even if they never scroll to or look at the map.

This is especially wasteful on service/organization detail pages: the map (MapOfLocations) is rendered in the "Location and Hours" section, well below the fold, after About, Service Details, and Contact Info. Any visitor who doesn't scroll that far still triggers a billed map load.

Change

Added a small useInView hook (app/hooks/useInView.ts) using the native IntersectionObserver API (no new dependency). It reports once when a ref'd element scrolls near the viewport, then stops observing — intended for one-time lazy loads like this rather than continuous visibility tracking.

  • MapOfLocations (service/org detail pages): now defers mounting <GoogleMap> until its container is within 300px of the viewport, showing the existing Loader in the same fixed-height .map container until then (no layout shift). This is the primary win — this section is reliably below the fold on both mobile and desktop.
  • SearchMap (search results page): applied the same pattern for consistency/defense-in-depth. Note this map is usually within the initial viewport by design (it's a persistent panel alongside the results list on desktop, and above the list on mobile), so the savings here will likely be smaller in practice. A bigger reduction on this page (e.g. click-to-load, or defaulting to list-first view on mobile) would be a product decision beyond a pure viewport check, and is out of scope for this PR.

Testing

  • npx tsc --noEmit — passes
  • npx eslint on changed files — passes
  • npx prettier --check on changed files — passes
  • Existing test suite (npm test) has no spec files covering these components; the harness itself currently fails to boot in newer Node versions unrelated to this change (repo pins Node 18 via .nvmrc).

Follow-ups not included in this PR

While researching this, I also found:

  • A real Google API key committed in plaintext in config.example.yml, and a second, different leaked key in a commented-out <script> tag in app/index.html. Worth rotating/removing and adding HTTP-referrer restrictions in GCP console regardless of this change.
  • StreetViewImage and the Static Maps image in ServicePdfPage also fetch directly from Google with no caching layer, and could be moved behind a backend cache/proxy since they're deterministic per-address.

Happy to open separate PRs/issues for those if useful.

Both SearchMap and MapOfLocations mount a <GoogleMap> unconditionally
on every page render, which fires a billable Maps JavaScript API
"map load" event even for visitors who never scroll to or look at
the map. This is especially costly on service/organization detail
pages, where the map is rendered well below the fold (after About,
Details, Contact Info, etc).

Add a small useInView hook (native IntersectionObserver, no new
dependency) and use it to defer mounting <GoogleMap> until its
container is about to scroll into view (300px rootMargin so it loads
just ahead of the user reaching it). A lightweight Loader placeholder
of the same size is shown until then, so there's no layout shift.

- MapOfLocations (service/org detail pages): this is the primary win,
  since this section is reliably below the fold.
- SearchMap (search results page): applied for consistency/defense in
  depth, though this map is usually within the initial viewport by
  design (a persistent map alongside/above the results list), so
  savings here will be smaller. A bigger reduction here would require
  a product decision (e.g. click-to-load, or a list-first default on
  mobile) rather than a pure viewport check.
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

Successfully merging this pull request may close these issues.

1 participant