WhollySoftware
Back to blogWeb

Optimizing Time to Interactive on Content-Heavy Landing Pages

Wholly Software TeamMarch 11, 20268 min read
Optimizing Time to Interactive on Content-Heavy Landing Pages

A client's flagship landing page had a deceptively good-looking Largest Contentful Paint of 1.2 seconds, but Time to Interactive was closer to 5.3 seconds — the page looked done well before it actually was, and users clicking the hero CTA during that gap got no response, which shows up in analytics as rage clicks and, eventually, as people leaving. This gap between 'looks loaded' and 'is usable' is exactly what TTI measures and LCP alone hides.

The cause was a large JavaScript bundle that included the entire page's interactivity — hero CTA handlers, a pricing calculator widget, a video modal, and a chat widget — all bundled together and all needing to parse and execute before any of it, including the simple 'click to scroll to pricing' button, became responsive. We split the bundle so the hero CTA's handler, the single most important interaction on the page, loaded in its own small chunk with no dependency on the pricing calculator or chat widget code.

We also found a common but easy-to-miss cause of TTI blowup: a third-party font loading strategy that used @import inside a CSS file, which is render-blocking and, worse, wasn't discovered by the browser's preloader until the CSS itself had already been fetched and parsed, adding a full extra round trip before fonts even started downloading. Switching to a preloaded link tag for the font files and font-display: swap in the CSS removed a roughly 400ms delay that had nothing to do with JavaScript at all.

For the pricing calculator and video modal — genuinely heavy, interaction-dependent widgets — we deferred their JavaScript entirely until the user scrolled near them or clicked a trigger, rather than loading and hydrating them upfront on a page where most visitors would spend their first several seconds only near the hero. This is the same principle as code-splitting by route, applied within a single long page instead.

React hydration itself was contributing too — the whole page hydrated as one tree, meaning the hero's interactivity was gated behind successfully hydrating components far below the fold that the user hadn't scrolled to yet. We're using selective/progressive hydration patterns (partial hydration for below-the-fold sections, hydrating on visibility via IntersectionObserver) rather than a single hydrateRoot call for the entire page.

After these changes, TTI dropped from 5.3 seconds to about 1.8 seconds, closer in line with the LCP number, and the client's conversion rate on the hero CTA specifically — the interaction most likely to be attempted during that dead window — improved by roughly 11%.

Web PerformanceTime to InteractiveReactCore Web Vitals
Optimizing Time to Interactive on Content-Heavy Landing Pages — Wholly Software