This site (JSkelet)
measured45.0 kB
gzip · first-load client assets
- Stylesheet (Tailwind v4 output) 13.1 kB
- Client entry (island loader) 301 B
- Icon sprite 3.9 kB
- Every island (on demand, separate chunks) 27.7 kB
Choose deliberately
See the strengths and the gaps of four approaches on the same ground. We are not hiding the rows JSkelet loses.
| Ölçüt | JSkelet | Next.js (App Router) | Astro + adapter | Express + EJS (by hand) |
|---|---|---|---|---|
| Client JS on an empty page | One entry; nothing else without islands | React plus runtime, even on a static page | None, unless you pick a UI framework | None (and no interaction either) |
| Framework JSON in the HTML | None — the document is the page | __NEXT_DATA__ (or RSC flight) alongside HTML | None by default | None |
| Interaction model | Vanilla islands with a mount function | Component tree with a server/client boundary | Islands in the UI framework of your choice | Script tags you wire up yourself |
| HTML caching | In-process TTL with stale-while-revalidate | ISR, tied to the platform and its storage | Static build or the adapter's CDN | None; you build it yourself |
| Targeted invalidation | Path, pattern or RegExp via invalidateHtmlCache() | Supported by tag and by path | A rebuild, or whatever the adapter offers | None |
| Routing | An explicit table; paths are written down | File system | File system | An explicit table |
| Streaming and partial render | Missing; slow sections move to fragments | Supported through streaming boundaries | Server islands and deferred content | By hand |
| Build chain | esbuild and Tailwind v4; optional steps skip themselves | A powerful bundler with a wide config surface | Vite | Whatever you assemble |
| Types | Plain JS with JSDoc; no compile step | TypeScript as a first-class citizen | TypeScript as a first-class citizen | Up to you |
| Per-session page HTML | Not cacheable; move it to a fragment | Supported | Supported in SSR mode | Supported |
+ an advantage of this approach − a deliberate or structural gap • neutral, simply different
Right now, in this browser
Same template, same bytes, same network — and an 80 ms simulated API inside the producer. A hit skips that wait; a miss pays it every time.
Served from cache
measuring…
/_fragment/render-demo-cached — HIT skips the 80 ms upstream sleep; only the round trip remains.
Rendered every time
measuring…
/_fragment/render-demo — no-store — pays the simulated API on every request, then renders.
This section measures nothing when JavaScript is disabled; the rest of the page is unaffected.
Round trip dominates both totals — that is why two empty renders looked tied. The honest signal is server produce: ~0 ms on a hit, ~80 ms+ when the producer runs. On a real page that sleep is your database or CMS; the cache stops charging visitors for it.
View Source
What ships to the crawler and the first paint is the page — not a second copy of your props serialized into a script tag.
<main>…</main>
<script id="__NEXT_DATA__" type="application/json">
{"props":{"pageProps":{"posts":[…],
"user":null,"locale":"en"}},
"page":"/","query":{},"buildId":"…"}
</script>
<script src="/_next/static/chunks/…js">
</script>
Content is sent twice: once as HTML, again as JSON for hydration. Crawlers and humans both download the tax.
<main>
<h1>Latest posts</h1>
<article>…</article>
<div data-island="newsletter"></div>
</main>
<script type="module" src="/assets/js/main….js">
</script>
One document. Islands load only when a marker is on the page — no hidden state dump, no second representation of the tree.
Crawlers
See the same HTML humans see. Nothing waits on a client runtime to assemble the article.
First paint
No multi-kilobyte JSON blob competing with content on the critical path.
Cache
What you store is the finished page. Invalidation refreshes HTML — not a serialized React tree.
Measured weight
What the 'client JS on an empty page' row above actually looks like here — then an estimated Next.js App Router first-load for the same kind of page.
| Raw | gzip | |
|---|---|---|
| Stylesheet (Tailwind v4 output) | 77.4 kB | 13.1 kB |
| Client entry (island loader) | 564 B | 301 B |
| Icon sprite | 12.1 kB | 3.9 kB |
| Every island (on demand, separate chunks) | 72.0 kB | 27.7 kB |
| Total — if every island loads | 162.0 kB | 45.0 kB |
45.0 kB
gzip · first-load client assets
115.2 kB
gzip · first-load client assets
Estimates, not a build from this repo: rounded gzip figures for a minimal App Router page (React ~44 kB, Next client runtime ~48 kB, thin page chunk, Tailwind-scale CSS). Your Next app may be leaner or heavier — the point is the framework floor you pay before your own UI.
Decide
Most rows are neutral. The decision usually comes down to the last two.