How to Load Chat Widgets Only When Users Need Them
This guide applies the facade pattern from Third-Party Script Performance to live-chat widgets, within JavaScript Bundle Optimization & Code Splitting. Customer-support chat tools — Intercom, Zendesk, Drift, HubSpot, LiveChat and others — typically load 300–700KB of JavaScript, open WebSocket connections, inject iframes and run long tasks during page load, on every page, for every visitor. A small fraction of visitors ever open the chat.
The cost lands on Core Web Vitals in several ways: download bandwidth competes with LCP resources, startup evaluation adds long tasks that delay early interactions (INP), and late-injected launcher buttons can shift layout (CLS). The facade pattern fixes all three: render a lightweight look-alike button immediately, and load the real widget only when the user hovers or clicks it — or, as a compromise, after the page has been idle for a while.
Rapid Diagnosis
- Measure the widget's footprint. Network panel filtered by the vendor's domains; total transferred bytes and request count.
- Find its long tasks. In a throttled trace, group main-thread work by third-party; chat widgets often appear near the top.
- Check open rates. Analytics on chat opens per page view — usually a few percent or less.
- Check CLS from the launcher. A launcher button injected late into the corner can shift sticky footers or cookie banners.
Root Cause Analysis
1. Eager loading by default. Vendor install snippets load everything on page load, optimising for their feature readiness, not your metrics.
2. Heavy runtime. Widgets bundle UI frameworks, real-time messaging, emoji pickers and file uploaders — all loaded before anyone opens chat.
3. Persistent connections. WebSocket connections and polling keep the network and main thread busy throughout the visit.
4. Late layout insertion. The launcher appears when the script finishes, potentially moving other fixed elements.
Step-by-Step Resolution
1. Render a facade that looks like the launcher
<button class="chat-facade" type="button" aria-label="Open chat" hidden>
<img src="/icons/chat-bubble.svg" width="28" height="28" alt="">
</button>
<style>
.chat-facade { position: fixed; right: 20px; bottom: 20px; width: 56px; height: 56px;
border-radius: 50%; border: 0; background: #0466c8; color: #fff; }
</style>
<!-- trade-off: the facade must match the vendor's launcher size and position
exactly, or users see the button jump when the real widget replaces it. -->
Expected outcome: users see a chat button immediately with no third-party cost.
2. Load the real widget on intent
const facade = document.querySelector('.chat-facade');
facade.hidden = false;
let loading;
function loadChat({ open = false } = {}) {
loading ??= new Promise((resolve) => {
window.chatSettings = { app_id: 'abc123', hide_default_launcher: false };
const s = document.createElement('script');
s.src = 'https://widget.vendor.example/loader.js'; s.async = true;
s.onload = resolve;
document.head.append(s);
});
return loading.then(() => { facade.remove(); if (open) window.ChatVendor?.('show'); });
}
facade.addEventListener('pointerenter', () => loadChat(), { once: true });
facade.addEventListener('click', () => loadChat({ open: true }));
// trade-off: the first click waits for the widget to load (often 1-2s on
// mobile). Show a spinner in the facade so the user knows it is working; the
// hover preload hides most of the wait for desktop users.
Expected outcome: zero chat JavaScript at load; the widget appears within a second or two of the user asking for it.
3. Optionally load after sustained idle for proactive messaging
If the business relies on proactive chat prompts, load the widget after the page has been idle for a while (for example 10–15 seconds after load), not at load.
addEventListener('load', () => setTimeout(() => requestIdleCallback(() => loadChat()), 12000));
// trade-off: proactive prompts appear later than before. Agree the delay with
// the support team; most find that prompts after 15s of engagement convert better anyway.
4. Preserve unread-message state
Some users have ongoing conversations. If the vendor sets a cookie or local storage key indicating unread messages, check it and load the widget immediately for those users only.
Verification
In a throttled trace of a page view without interaction, no requests to the vendor should appear and the long tasks attributed to the widget should be gone. Lab TBT and the INP of early interactions should improve by roughly the widget's startup work. Click the facade and confirm the widget opens with conversation history intact. Track chat open rates before and after to confirm the support experience is unaffected.
Worked Example: A SaaS Marketing Site
A SaaS marketing site loaded its chat widget on every page. In lab tests on a mid-tier phone, the widget added 430KB and 640ms of main-thread time; field INP p75 was 240ms, with LoAF attribution naming the widget's scripts in a third of slow interactions during the first ten seconds. A facade with hover preloading and click-to-open, plus a 15-second idle load on pricing pages only (where proactive chat mattered to sales), brought site-wide INP p75 to 170ms and LCP p75 down by 180ms. Chat conversations per visit were unchanged; conversations started from the pricing page rose slightly, because proactive prompts now arrived after users had read the page.
Common Mistakes
- A facade that does not match the real launcher. Size or position differences cause a visible jump and potential CLS.
- Forgetting accessibility. The facade must be a real button with an accessible name and keyboard support.
- Loading on scroll. Scrolling is not intent to chat; it loads the widget for most visitors anyway.
- Ignoring logged-in or ongoing conversations. Users with unread messages expect the widget to show them.
Edge Cases
Vendors that require a boot call. Some widgets need boot() with user identity after load; pass identity data in the deferred loader, not in an eager inline snippet.
Single-page apps. On route changes, keep the facade or widget mounted once rather than re-injecting it per route.
Consent requirements. If chat sets non-essential cookies, loading on click can double as the consent moment, which simplifies compliance for some setups — confirm with your legal team.
Mobile keyboards. On mobile, opening the widget triggers the keyboard; make sure the facade's spinner state does not leave users tapping repeatedly.
FAQ
Do chat vendors support delayed loading officially?
Many expose options to hide the default launcher and to boot programmatically, which is all a facade needs. Some publish lazy-loading guides; others' snippets assume eager loading but work fine when injected later. Test the vendor's features (unread badges, proactive messages) after the change.
Will deferring chat affect support metrics?
Usually not in ways users notice, since few open chat in the first seconds of a visit. Proactive messages are the main change; tune their timing with the support team and compare engagement before and after.
Can Partytown run the chat widget in a worker instead?
Chat widgets are DOM-heavy and interactive, which is a poor fit for worker-based execution. A facade is simpler and more reliable for this category; worker offloading suits tag-like scripts better.
What about help-centre search widgets?
The same pattern applies: render a static search box or button, load the vendor script on focus or click. Anything that is not needed until the user acts is a candidate.
How do I keep the facade in sync with vendor design changes?
Vendors occasionally change their launcher's size, shape or position. Keep the facade's styles in one small component, add a visual regression test that compares it with the real launcher after loading, and review it whenever the vendor announces UI changes. A mismatch is cosmetic, but it causes a visible jump that users notice.
Related
- Replacing YouTube embeds with a facade — the same pattern for video embeds.
- Reducing input delay from third-party tags — measuring the INP effect of vendors.
- Reducing consent management platform cost — another always-loaded third party.