Back to Performance
Performance
medium
mid

How do you reduce Time to Interactive on a page?

TTI is the moment the page is reliably responsive. The killer is JS — download, parse, execute, hydrate. Reduce by shipping less JS (RSC, code splitting, tree shaking), avoiding long tasks, deferring non-critical work, and hydrating selectively.

7 min read·~15 min to think through

TTI is the time from navigation start until the page is reliably responsive to input — usually defined as no long task in the next 5 seconds plus all critical scripts loaded.

What makes TTI slow:

  1. Big JS bundles — download time on slow networks dominates.
  2. Long tasks — any task > 50ms blocks input. Hydration is the most common culprit.
  3. Render-blocking resources — synchronous scripts, non-critical CSS in the head.
  4. Third-party scripts — analytics, chat, ads run on the main thread and steal CPU.

The fix list:

  • Ship less JS. Tree shaking, dead-code elimination, RSC (server-only code stays on server), heavy libs (charts, editors) lazy-loaded.
  • Code-split by route. Each route loads its own chunk, not the whole app.
  • Defer non-critical scripts. defer / async / requestIdleCallback / scheduler.postTask. Move analytics off the critical path.
  • Reduce hydration cost. Selective hydration (Astro islands, RSC, partial hydration). Only hydrate interactive components.
  • Break long tasks. yieldToMain patterns inside long loops.
  • Preconnect / preload critical origins early.
  • Move third-party off main thread. Partytown for analytics, web workers for CPU.

Measure: Lighthouse for lab data, web-vitals library for field. The new metric to watch alongside TTI is INP (Interaction to Next Paint) — captures the worst real-user interaction, not just initial load.

Code

ts
function yieldToMain() {
  return new Promise<void>((r) => setTimeout(r, 0));
}

async function processBatch(items: Item[]) {
  for (let i = 0; i < items.length; i++) {
    process(items[i]);
    if (i % 100 === 0) await yieldToMain();
  }
}
yieldToMain — break a long synchronous task

Follow-up questions

  • Why does hydration cost so much, and how does selective hydration help?
  • How does the React Compiler change the JS-payload story?
  • What's the cheapest third-party-script optimization?

Common mistakes

  • Shipping a 500KB main bundle and trying to fix TTI without code-splitting.
  • Lazy-loading critical components so the LCP element waits on a chunk.
  • Letting third-party scripts run synchronously in the head.

Performance considerations

  • Server-render the LCP element as plain HTML, hydrate later. The user sees the page much sooner.

Edge cases

  • Single-page app navigations don't have a 'fresh TTI' — measure long tasks per route change.
  • Slow CPU emulation in DevTools reveals issues that don't show on dev hardware.

Real-world examples

  • Astro's 'islands' architecture and Next.js RSCs were both motivated by hydration-cost-driven TTI.

Senior engineer discussion

Senior signal: discuss perf budgets in CI, RUM-driven optimization, RSC streaming, and how INP supplements/supersedes TTI in the modern toolkit.

Related questions