← All posts

How we made the Lavenity launcher appear in under 100 ms

A 2.9 KB gzip loader, immediate launcher render, idle iframe preload, and a measured local paint time below 10 ms keep chat from slowing down the host page.

A support widget should be available quickly without competing with the page it is meant to help. We rebuilt the Lavenity loading path around that boundary: paint the small launcher first, then prepare the full conversation experience when the browser has room.

Why the launcher loads separately

The embed loader is 8,058 bytes uncompressed and 2,947 bytes with gzip in the current production build. It is loaded with an async script tag, injects a small isolated stylesheet, and creates the launcher with plain browser APIs. Vue, conversation history, WebSocket setup, and remote workspace configuration are not required before the button can appear.

The heavier chat application remains isolated in an iframe. After the launcher mounts, Lavenity asks requestIdleCallback to preload that frame, with a timeout as a fallback. This keeps the frame away from the host page’s first render while making the first open feel ready. If a visitor clicks before the idle task runs, the same idempotent frame setup starts immediately.

What we measured

We also changed caching for the fixed widget.js URL. The loader can stay fresh for five minutes and be revalidated in the background for a day. Repeat visits can therefore render from cache without waiting for a blocking validation request, while loader updates still propagate quickly.

On 21 July 2026 we ran five cache-busted local Chromium measurements from script insertion to launcher readiness. The launcher entered the DOM in 2.9–3.6 ms and reached the next paint in 4.4–8.8 ms. Those numbers isolate browser execution on a local server; they do not include a real visitor’s DNS, TLS, geographic latency, device load, or CDN path.

Caching, iframe loading, and the real speed budget

That leaves a practical budget of roughly 100 ms for the launcher on a fast connection or cached visit, rather than turning 100 ms into a universal promise. Real-world timing will vary, so we will keep measuring production percentiles as traffic grows. The useful engineering result is that local work takes less than 10 ms and network transfer is only 2.9 KB gzip.

The visible button is now the cheapest part of the experience, and the full app waits its turn. That is the behavior we want from embedded support: present when the customer needs it, quiet while the rest of the site is loading.

How to test the chat widget on your own site

Measure at least two states: a first visit with a clean cache and a repeat visit. Check when the launcher appears, changes to Largest Contentful Paint and Total Blocking Time, and the time needed to open the full chat. One local run cannot represent a mobile network, a slower device, or geographic latency.

Compare the same page under the same conditions instead of comparing headline scores from unrelated runs. If the widget is installed through a tag manager, include the container delay. The launcher may execute quickly but appear late because of tag order, consent rules, or other scripts ahead of it.

Frequently asked question

Does “under 100 ms” mean the same time for every visitor?

No. It is a practical budget for a fast or cached visit, not a universal guarantee. Real timing depends on the network, device, CDN, script order, and page load. In our local measurements, launcher execution itself remained below 10 ms.