跪拜 Guibai
← All articles
Frontend

Measured Time Budgets Show SSR Moves the Cost Into an FCP-to-TTI Canyon

By 前端阿凡 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Teams tune TTFB with SSG or streaming while TTI stays stuck on hydration, and a single ISR config can mean 30-second staleness on hot pages but 45-minute staleness on cold ones. These measurements make both failures visible before deployment and separate what actually needs changing: client JS execution, not render location.

Summary

Eight rendering strategies share one page model in a deterministic Node time-budget script: 2400 components, 9000 DOM nodes, 700KB raw / 230KB gzip JS, 40ms RTT, and 4x CPU throttle. The output separates the blank period (navigation to first content) from the canyon (first content to interactive). CSR leaves users staring at white for 945.8ms but only 120.0ms of unclickable content. SSR cuts FCP to 541.4ms and turns the canyon into 1346.8ms; TTI lands at 1888.2ms, 822.5ms later than CSR. Streaming and SSG move TTFB forward (by 229ms and 264ms respectively) without moving the canyon at all. The deciding account is JS: transfer 153.3ms, parse 210.0ms, hydration 1215.0ms.

A 4000-click simulation of the canyon finds 9.8% of clicks before the root listener exists do nothing; queued clicks wait 417.6ms on average, 827ms at P95, and 1179ms worst-case. Islands, RSC, and resumability shrink the canyon to 151.9ms, 91.1ms, and 39.0ms by cutting hydration work. Island economics follow a 'constant + interaction ratio' curve: one framework runtime is 46KB gzip no matter how few components are interactive, three frameworks ship 138KB gzip, and five frameworks equal the full-hydration JS payload.

Under ISR, the same revalidate=60 config produces a 90.9x spread between hot and cold pages: 30s average staleness on the Top1 page, 45.5min on the coldest of 10000 Zipf-distributed pages. 9933 pages revalidate as often as SSR would and gain 24.3 minutes of extra staleness. Revalidation storms only appear under event traffic: 60 concurrent renders at 200 req/s versus 0.22 at normal rates. Request coalescing protects the origin; SWR protects users. On-demand invalidation uses the same 24 origin hits/day as revalidate=3600 and holds worst-case staleness at 2s instead of 1h.

Takeaways
— On the modeled page, SSR moves FCP from 945.8ms to 541.4ms versus CSR, but TTI goes from 1065.8ms to 1888.2ms (822.5ms later, +77.2%).
— SSG and streaming shift TTFB, FCP, and TTI earlier by 264.0ms or 229.0ms, but leave the FCP-to-TTI canyon at 1346.8ms; delta canyon = 0.0ms.
— The JS account in the model is transfer 153.3ms, parse 210.0ms, hydration 1215.0ms; hydration is 7.9x transfer and 76.7% of the whole JS account.
— Halving the gzipped JS size halves transfer time and leaves parse time exactly unchanged, because parsing runs on decompressed bytes.
— A 4000-click simulation in the canyon finds 9.8% of clicks hit no event handler, 36.4% are queued and replayed, and the 53.9% that respond immediately leave queued clicks waiting 417.6ms average, 827ms P95, 1179ms worst.
— With islands or resumability, 100% of clicks in the same window respond immediately; their TTI falls inside the first frame.
— Island JS equals a constant runtime plus interactive components: at 0% interactive, 46KB gzip still ships; at 100% interactive, islands ship exactly the full-hydration JS.
— Three framework runtimes cost 138KB gzip, more than a full single-framework island page's 69KB; five runtimes equal the full-hydration payload at 230KB.
— Under revalidate=60 on 10000 Zipf pages, the Top1 page averages 30s staleness and the coldest page averages 45.5min (90.9x); 9933 pages save zero origin hits versus SSR and add average 24.3min staleness.
— On low-traffic pages, raising revalidate from 60s to 3600s does not reduce origin revalidations; average staleness grows from 45.5min to 1.2h because request interval decides staleness.
— ISR with revalidate=3600 plus SWR costs 24 origin revalidations/day and averages 30min staleness; ISR with on-demand invalidation also costs 24 revalidations/day and averages 1s, worst 2s.
— Full static generation for 10000 pages takes 38min; 24 builds per day is 15.2h, while an incremental build touching 0.5% of pages takes 11.3s.
— Root-level hydration mismatch fallback costs +660ms versus +28.5ms for subtree-level fallback; at 1% mismatch, average cost is 6.6ms but 1000 of 100000 daily users see the full 660ms regression.
Conclusions

Render location is easy to judge by TTFB and FCP because those numbers appear early in a trace; TTI arrives later, so 'SSR made the page faster' can survive without anyone checking interaction latency.

ISR's freshness numbers are two different averages depending on what is weighted: page-weighted staleness is 24.2 minutes, traffic-weighted is 7.7 minutes. A dashboard that averages the wrong axis will certify either version as healthy.

SSR versus CSR is a unit-cost comparison, not an architectural identity. The model solves for a 0.15 ms/node crossover, so pages dominated by static DOM can favor SSR while JS-heavy pages can favor CSR.

Islands and RSC still pay a fixed framework floor that no build optimization removes; every added framework consumes the savings, and three runtimes already exceed the entire island payload of a single-framework page.

One percent hydration mismatch is invisible to a mean-based performance board but hits a thousand users per day with a 660ms reset; that failure mode is variance, and the standard aggregate hides it.

Concepts & terms
Canyon (峡谷)
The interval between first content (FCP) and time-to-interactive (TTI) in the article's model. The page is visible but interactions are dropped, queued, or replayed; the width is set by JS work, not by where rendering happened.
Hydration
The client-side process that re-creates framework state and attaches event listeners over server-generated HTML. In the model it costs 0.45 ms per component plus 0.015 ms per node and is usually the largest term in TTI.
Stale-while-revalidate
ISR's revalidation semantics: a stale copy is returned immediately after expiry, and the next request triggers background regeneration. Because the trigger is the next request, average staleness depends on request rate, not just the configured revalidate window.
Resumability
Qwik's alternative to hydration: listeners, component tree/boundary data, subscriptions, and app state are serialized into HTML and QRLs, and interaction handlers resume on demand instead of replaying the whole tree. The article calls it 'zero replay, not zero cost'.
Runtime floor
The fixed framework JS that must ship even when only some components are interactive. With one framework it is 46KB gzip in the model; with three frameworks it is 138KB, and with five it equals the full-hydration payload.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗