A server-rendered page shows its content in about a second, but clicks and taps do nothing for another two seconds. The trace shows one roughly 1.8 s main-thread task starting right after the JavaScript bundle finishes downloading. What is usually happening, and what are your options?
answer
- visible early, usable late
- the gap between paint and interactive
- the bundle finished downloading, then what?
- attaching behaviour to markup you did not create
- compile, bootstrap, hydrate, parse state
basics
~20 sThat gap between visible and usable is the classic hydration cost: the server HTML paints, then one long task compiles and evaluates the bundle and attaches behaviour to the existing markup, blocking input until it finishes. The fixes shrink or split that work; making the download faster does not help.
solid answer
~60 sContent appearing before it works is the signature of server-rendered HTML plus a large client bootstrap. The server sends markup the browser can paint immediately, then the bundle arrives and one uninterrupted task compiles it, runs the framework's bootstrap, walks the server-rendered markup attaching event listeners and rebuilding component state, and parses whatever initial state was embedded in the document. None of that can be interrupted, so every click during it is stranded. I would first confirm the split inside that 1.8 s — script compile and evaluate versus bootstrap versus state parsing — since the fixes differ. Then, in order of payoff: ship less JavaScript to that route, hydrate only the regions that are genuinely interactive and defer the rest until visible or idle, shrink the embedded state payload, and serve it as a JSON string rather than an object literal. What will not help is a faster CDN, better compression, or moving the tag to `defer` — those change when the bytes arrive, not how long the CPU work takes.
code
html · 8 lines<script type="application/json" id="initial-state">
{"user":{"id":7,"name":"Ada"},"items":[{"id":1,"title":"First"}]}
</script>
<script>
window.__INITIAL_STATE__ = JSON.parse(
document.getElementById('initial-state').textContent
);
</script>go deeper
Know that a page can look finished while its JavaScript has not run yet, and that clicks do nothing until the browser has executed that code and attached the behaviour.
Be able to name what hydration does — walk the server-rendered markup, attach listeners, rebuild state — and explain why it is CPU work that a faster network cannot shorten.
Expect to be judged on the diagnosis. Split the long task into compile, bootstrap, hydrate and state-parse before proposing anything, and rank fixes by payoff while naming the popular ones that do nothing here.
Own the architectural call: whether a mostly-static page should be paying full client bootstrap at all. Be ready to weigh a move to partial hydration against its migration cost and to define what interactivity target the team commits to.
## Reading the symptom first The shape of this report is diagnostic on its own. **Visible early, usable late** means the rendering path is healthy and the JavaScript path is not. If the stylesheet were render-blocking or the server slow, nothing would paint at a second. If images were the issue, controls would still respond. A single long task starting exactly when the bundle finishes downloading points at one thing: everything the client does to take ownership of markup it did not create. ## What is inside that task Four distinct costs usually hide in one block: 1. **Compile and evaluate.** The engine must parse and compile the bundle before any of it runs, and top-level evaluation runs every module's side effects — registrations, singleton construction, polyfill installation. 2. **Framework bootstrap.** Building the initial component tree in memory. 3. **Hydration proper.** Walking the server-rendered DOM, matching it against what the client would have produced, and attaching event listeners and state. This scales with the *amount of markup*, not the amount of code. 4. **Initial state deserialization.** Server-rendered pages usually embed the data used to render, so the client does not refetch it. On a data-heavy page this blob can be hundreds of kilobytes. ## Confirming which one dominates Do not guess — the fixes are different. In a trace, script compilation and evaluation appear separately from the function calls that follow, and the state parse shows up as its own block. In the field, frame-level performance entries can name the script and function that dominated. `performance.mark` and `performance.measure` around your own bootstrap phases give you the split cheaply and work everywhere. ## The embedded-state detail One concrete and frequently-missed win: how the initial state is embedded. A large JavaScript object literal inside an inline script must be handled by the full JavaScript parser, with all its ambiguity. The equivalent data as a JSON *string* passed to `JSON.parse` uses a far simpler grammar and is typically several times faster for large payloads. The change is mechanical: ```html <script type="application/json" id="state">{"user":{"id":7},"items":[]}</script> <script> window.__STATE__ = JSON.parse(document.getElementById('state').textContent); </script> ``` It costs nothing and can take a meaningful bite out of a large blob's parse time. It does not make the payload smaller, though — that is a separate conversation about sending only the fields the first view needs. ## Options, in order of payoff **Ship less JavaScript to this route.** The most reliable fix. Everything in the initial payload gets compiled and evaluated whether or not the first view uses it. Route-level splitting and moving rarely-used features behind dynamic loading directly shrink both the compile and the evaluate cost. **Hydrate less.** Most pages are mostly static. Architectures that hydrate only interactive regions — islands, partial or selective hydration, depending on your framework's vocabulary — cut hydration cost roughly in proportion to how much of the page is genuinely interactive. This is a structural change, not a flag, and it is usually where the big wins are on content-heavy pages. **Defer what is not needed yet.** Regions below the fold do not need to be interactive at first paint. Deferring their hydration until they scroll into view, or until the main thread is idle, removes their cost from the blocking window entirely. **Shrink the state payload.** Send only what the first render needs and fetch the rest afterwards. Halving the blob halves its parse cost and its transfer cost at once. **Break up what remains.** If a large bootstrap is unavoidable, splitting it so the browser can dispatch input between pieces converts a dead page into a slow one — a real improvement in perceived responsiveness, though it does not reduce total work. ## What will not help Be explicit about this in an interview, because it is where weak answers go: - A **CDN** or **better compression** changes when the bytes arrive. The task starts after the download either way and takes the same time. - **`defer` or `async`** on the script tag moves the work; it does not shrink it. On a page whose paint already succeeded, it changes very little. - **A faster server** improves the time to first paint, which was never the complaint. - **Preloading the bundle** makes the CPU work start *sooner*, which can look better on a load metric and feels identical or worse to a user who is trying to click. ## The honest ceiling Sometimes the answer is architectural: if a page is fundamentally a document with two interactive widgets, paying full client-side bootstrap for it is the wrong design, and no amount of tuning inside that 1.8 s task will fix it. Saying so — and quantifying what a different approach would cost — is the senior part of this answer.
- How would you distinguish script compile-and-evaluate cost from hydration cost inside that single task?Compilation and top-level evaluation appear as their own entries in a trace, distinct from the function calls that follow. You can also reason by scaling: compile and evaluate track the size of the bundle, while hydration tracks the amount of server-rendered markup. Rendering the same route with far less content and re-measuring separates the two quickly.
- Users on fast desktops never report this, but mid-range phone users do. Why is the gap so much wider there?The download is roughly the same, but parsing, compiling and executing JavaScript is CPU-bound, and a mid-range phone can be several times slower per core than a developer laptop while also thermally throttling. Main-thread work scales with device speed in a way transfer time does not, which is why lab runs use CPU throttling and why field data is segmented by device class.
- Does making the page fully client-rendered instead of server-rendered remove the problem?No — it moves it. You lose the early paint, so instead of visible-but-dead the user gets nothing until the bundle runs. The bootstrap cost is still there, minus the hydration matching. Server rendering is what bought the early content; the defect is the size of the client work that follows it, and that is what has to shrink.
saying these in an interview costs you the question
- Blames the network and proposes a CDN or Brotli
- Suggests defer or async on a page that already painted
- Claims preloading the bundle fixes the interactivity gap
- Thinks a faster server shortens the blocking task
- Assumes any page that paints quickly is fast