Facades for Chat Widgets and Maps

Some third-party embeds are heavy for a reason: a live chat widget is a small application with its own framework, websocket connection and styles; an interactive map loads a tiling engine, vector data and fonts. On a static site, where the rest of the page might total 150 KB, one of these can add 400–800 KB and several hundred milliseconds of main-thread work — on every page view, for every reader, although only a few percent of readers ever open the chat or pan the map.

A facade loads the lightweight part that readers see — a chat button, a static map image — and loads the real embed only when a reader shows they want it. This guide builds facades for an Intercom-style chat widget and a Google Map on a static marketing site, warms them up on hover and focus so they feel instant, handles accessibility and consent, and measures the savings. It is part of Third-Party Script Performance on Static Sites, alongside Lazy-Loading YouTube Embeds on Static Sites.

Prerequisites

  • An inventory of heavy embeds and how often readers use them (analytics events for opening chat, interacting with the map).
  • The vendors' loader snippets and APIs.
  • A consent mechanism, if the site needs one for third-party cookies.

What the Embeds Cost

Measured with Lighthouse (mobile, median of five) and the request-blocking method from Measuring Third-Party Cost with Request Blocking:

EmbedPagesBytes (compressed)Main-thread timeUsed by
Chat widgetall 180312 KB410 ms1.8% of sessions
Google Map (contact, locations)6590 KB520 ms11% of views of those pages

The chat widget alone accounted for most of the Total Blocking Time on every page, and its loader created a websocket and set cookies on page load.

Bytes and main-thread time of the chat widget and map before facades Horizontal bars. The Google Map embed cost 590 kilobytes and 520 milliseconds of main-thread time on the pages that had it. The chat widget cost 312 kilobytes and 410 milliseconds on every page. Only 1.8 percent of sessions opened chat and 11 percent of map page views interacted with the map. Cost of each embed on page load Map: bytes (KB) 590 Map: main thread (ms) 520 Chat: main thread (ms) 410 Chat: bytes (KB) 312
Lighthouse mobile median of five runs; cost measured by comparing runs with the embed blocked and unblocked.

Facade 1: The Chat Button

The facade is a button styled like the vendor's launcher, rendered in the static HTML with a few lines of CSS. The vendor script loads on first click — or earlier, when the pointer approaches the button or it receives keyboard focus:

<button type="button" class="chat-facade" aria-label="Open chat with support" hidden>
  <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="24" height="24"><path d="M4 4h16v12H7l-3 3z" fill="currentColor"/></svg>
</button>
<script type="module">
  const btn = document.querySelector('.chat-facade');
  btn.hidden = false;
  let loading;
  const load = () => loading ??= new Promise((resolve) => {
    window.intercomSettings = { app_id: 'abc123', hide_default_launcher: true };
    const s = document.createElement('script');
    s.src = 'https://widget.intercom.io/widget/abc123'; s.async = true; s.onload = resolve;
    document.head.append(s);
  });
  btn.addEventListener('pointerenter', load, { once: true });
  btn.addEventListener('focus', load, { once: true });
  btn.addEventListener('click', async () => {
    btn.setAttribute('aria-busy', 'true');
    await load();
    window.Intercom('show');
    btn.remove();
  });
</script>

The button is hidden until the script runs, so readers without JavaScript do not see a control that does nothing; they get a "Contact support" link in the footer instead. hide_default_launcher stops the vendor from drawing a second button. After loading, the vendor's own launcher replaces the facade for the rest of the session.

On a desktop with hover warm-up, the chat panel opened 180 ms after click on a fast connection — faster than before, because the loader had already finished. On touch devices without hover, the first open took about 900 ms on 4G, shown with a loading state on the button.

Sequence of loading the chat widget through a facade The page loads with only the facade button and no vendor requests. When the pointer approaches the button, the vendor script starts loading. When the reader clicks, the facade waits for the load if needed, calls the vendor API to open the panel and removes itself. Readers who never approach the button never trigger any vendor request. The chat facade from hover to open Reader pointer or keyboard Facade static button Vendor chat script + socket page load, no vendor requests pointerenter or focus load script (warm-up) click Intercom(show), facade removed
98.2% of sessions never load the vendor script at all.

Facade 2: The Map

The contact page embedded an interactive Google Map to show the office. The facade is a static map image generated at build time from the Maps Static API, stored in the repository and optimized like any other image, with a link that opens the map in a new tab and a button that loads the interactive map in place:

<figure class="map-facade">
  <img src="/img/map-berlin-800.avif" srcset="/img/map-berlin-400.avif 400w, /img/map-berlin-800.avif 800w, /img/map-berlin-1200.avif 1200w"
       sizes="(min-width: 48em) 40rem, 100vw" width="800" height="450" alt="Map showing our office at Torstraße 49, Berlin, near Rosenthaler Platz" loading="lazy">
  <figcaption>
    <a href="https://maps.google.com/?q=Torstra%C3%9Fe+49+Berlin" rel="noopener">Open in Google Maps</a>
    <button type="button" class="map-load">Show interactive map</button>
  </figcaption>
</figure>

Clicking the button replaces the image with the embed iframe at the same aspect ratio, so nothing shifts. Most readers only needed the address and a link to their own maps app, which the facade provides with one tap; 89% of views of the contact page never loaded the interactive map. Generating the static image at build time, rather than hotlinking the Static API, keeps the page free of third-party requests and lets the image pipeline serve AVIF at the right sizes.

Facades must be real controls. The chat facade is a <button> with an accessible name that says what it does, not "chat bubble icon"; aria-busy communicates loading; focus moves into the vendor panel after it opens, and the vendor's close button returns focus to the page. The map's alt text describes the location, which is the information the map conveys.

For consent, the facade is the natural place to ask. The site's consent banner covers analytics; for the map, the interactive button's first click shows a short inline notice ("Loading Google Maps sends data to Google") with confirm and cancel, and remembers the choice. No Google request happens before that. The chat vendor is covered by the site's privacy notice and loads only on explicit interaction, which the legal review accepted without a separate prompt.

Experience of readers who use the embeds and those who do not Two panels. Readers who never use chat or the map, the vast majority, get pages with no vendor scripts, no vendor cookies and faster loading. Readers who do use them see a styled button or static map that looks like the real thing, the embed loads on hover, focus or click, and the first open takes up to about 900 milliseconds on touch devices. What each reader group experiences Most readers (never use it) no vendor scripts or cookies TBT down 380 ms on every page address and link without loading a map Readers who use it facade looks like the real control loads on hover or focus first open ~180 ms desktop, ~900 ms touch
The trade is a short delay on first use for a few readers against faster pages for everyone.

Other Candidates for Facades

The same pattern applies to most embeds that are visible on load but used by a minority: social media post embeds (render the post text and author as static HTML at build time, load the vendor script on click), code playgrounds (show the code with a "Run" button that loads the sandbox), booking and calendar widgets (a styled "Pick a time" button), comment systems (a "Show 14 comments" button that loads the thread), and audio players from hosting platforms (a play button and duration from build-time metadata). Each one follows the same steps: measure how many readers actually use it, render a static stand-in that carries the useful information, load the real embed on explicit intent, and warm up on hover or focus where a delay would be noticeable. On this site, the comments embed on blog posts was the next facade after chat and maps; 6% of readers opened comments, and the blog template lost another 140 KB of JavaScript.

Measured Impact

Two weeks of field data and Lighthouse lab runs, before and after:

MeasureReal embeds on loadFacades
JS on a typical page (brotli)348 KB36 KB
Total Blocking Time, lab470 ms90 ms
INP p75, field230 ms110 ms
LCP p75 contact page, field3.1 s1.9 s
Third-party requests on page load230
Chat conversations started per week214221

Chat usage did not fall — it rose slightly, possibly because pages became responsive sooner. The support team had worried that a facade would reduce conversations; the data settled the question.

Pitfalls & Rollback

  • A facade that looks clickable before it works. Hide it until its script runs, and provide a no-JavaScript alternative.
  • Double launchers. Tell the vendor not to render its own button until the facade hands over.
  • Hotlinking static map images. That is still a third-party request; generate the image at build time.
  • Missing required attribution. Keep the vendor's logo or notice where terms require it.
  • Rollback: the facade is one component per embed; restoring the vendor snippet in the layout brings back eager loading.

Conclusion

Heavy third-party widgets used by a small share of readers should not load for everyone. Facades — a styled button for chat, a static image for maps — keep the page light and load the real embed on interaction, warmed up on hover and focus. On this site they removed over 300 KB of JavaScript from every page, halved INP and took the contact page's LCP from 3.1 s to 1.9 s, without reducing chat usage.

FAQ

What is a facade?

A lightweight static stand-in for a heavy third-party embed, such as an image of a map or a styled chat button, that looks like the real thing and loads the actual embed only when the reader interacts with it or clearly signals intent, such as hovering or focusing the element.

Does a facade hurt the experience for readers who use the widget?

Slightly, the first time: the real widget takes a moment to load after the click. Warming up on hover or focus, and showing a loading state, keeps that delay short. For the majority of readers who never use the widget, the page is much faster.

Are facades allowed by the vendors' terms?

Usually yes, as long as you do not misrepresent the vendor or bypass required notices. Map providers often offer a static image API meant for exactly this. Check the terms for attribution requirements and keep required logos and notices on the facade.

Yes. Because the vendor's script does not load until interaction, no third-party requests or cookies occur for readers who never use the widget, which simplifies consent requirements. The facade can also show a consent notice before loading.

Filed underHow-tos