Optimizing Our Survey Widget for Sub-100ms Load Times
Our widget runs on other people's websites, which means every millisecond we add is a millisecond subtracted from their page performance score — and, indirectly, their own SEO and conversion rates. Getting the widget under 100ms end-to-end load time required attacking the problem from multiple angles.
Async Loading by Default
The widget script tag uses the async attribute so it never blocks the parent page's rendering, even on slow connections. This was the single highest-impact change, immediately removing widget load time from the critical rendering path entirely.
Minimizing the Critical Bundle
The initial loader script is under 5KB gzipped and contains only enough logic to check eligibility and decide whether to fetch the full widget. The heavier rendering code only loads if the widget will actually display, which is the majority-case optimization since most page loads don't trigger a visible survey.
Edge Caching Configuration
Widget configuration is served from edge cache nodes geographically close to the visitor, cutting round-trip latency compared to hitting a single origin region. Cache invalidation on publish happens within seconds, so customers don't experience stale configuration after making changes.
Preconnect Hints
We instruct the loader script to emit a preconnect hint for our CDN domain as early as possible, letting the browser establish the DNS and TLS handshake in parallel with other page resources rather than sequentially when the widget actually needs the connection.
Measuring in Production, Not Just Locally
Synthetic lab tests don't capture the diversity of real-world conditions our widget runs under — flaky mobile connections, ad blockers, and heavily loaded pages. We collect real-user monitoring data from actual widget loads and treat the P95 load time, not the average, as our primary optimization target.
Regression Testing Performance on Every Deploy
Performance work is easy to lose gradually, one small feature addition at a time, if it isn't actively guarded. We run an automated performance budget check on every deployment to the widget codebase — any change that pushes the bundle size or measured load time above a defined threshold fails the build automatically, forcing an explicit decision about the tradeoff rather than letting performance erode silently over successive releases.
Getting under 100ms wasn't one big change — it was a dozen small ones, each shaving off a few milliseconds, applied consistently across every part of the loading pipeline.