Minimal & Fast
Complete HTML from the server. No React runtime, no hydration waterfall — only the bytes a content page needs.
A minimal, high-performance foundation for content sites. Complete HTML from the server, islands only where you touch, cache ready before the visitor asks.
Why JSkelet
The server prepares the content, the cache removes the waiting, and islands add only the behaviour a page actually needs.
Complete HTML from the server. No React runtime, no hydration waterfall — only the bytes a content page needs.
A menu, a counter or a chart lives in its own island. Its module downloads only when it becomes visible.
Plain async controllers, .jsk templates and a small API. Readable without ceremony, replaceable without drama.
Transparent performance
These numbers are read from the real build output and reflect the weight after gzip. Working code, not a marketing promise.
| Raw | gzip | |
|---|---|---|
| Stylesheet (Tailwind v4 output) | 79.4 kB | 13.9 kB |
| Client entry (island loader) | 593 B | 287 B |
| Icon sprite | 12.5 kB | 4.0 kB |
| Every island (on demand, separate chunks) | 73.5 kB | 28.4 kB |
| Total — if every island loads | 166.0 kB | 46.5 kB |
46.5 kB
Everything included (gzip)
Stylesheet, entry, sprite and every island on the site combined. No single page downloads all of it.
0
Web font requests
A system font stack. No FOUT and no font-driven layout shift.
1
Render-blocking request
One stylesheet. The client entry is a module, so it is deferred.
2
Islands loaded immediately
Theme and mobile menu. Every other island waits until it scrolls into view.
If you want a comparable number, measure your own project the same way: a claim of being lightweight only means something once it is measured on your page.
Simple by design
Stale-while-revalidate keeps visitors out of the render queue. One route, one TTL, predictable behaviour.
export default function register(app, { route, notFound }) {
app.get("/", route(
async () => ({
view: "pages/home",
data: { pillars },
}),
{ revalidate: 3600 },
));
app.get("/product/:slug", route(async ({ params }) => {
const product = await getProduct(params.slug);
if (!product) notFound();
return { view: "pages/product", data: { product } };
}));
}
Ops you can see
The HTML cache starts in process memory. Flip a switch and the same keys share across replicas, open a hardened panel, and talk to your edge.
Shared tier
L1 stays in process for microsecond hits. Redis holds the shared HTML/data bodies and fans invalidateHtmlCache() / clearHtmlCache() to every replica over pub/sub — ioredis is optional; missing it just means L1-only.
Primary store
In-process L1
Shared bodies
Redis HTML + data
Fan-out
Pub/sub invalidate
The right match
Public, cacheable pages are the center of gravity. Signed-in dashboards and per-visitor pages are supported with private: true — they skip the HTML cache. A React SPA or a built-in real-time client is not what this is.
FAQ
Most pages are not interactive. React's cost is fixed: the runtime downloads, a tree is built, hydration runs — and the result is the same HTML the server already produced. JSkelet removes that fixed cost and gives interaction only to the elements that need it.
The framework source is plain JavaScript with JSDoc and ships .d.ts for imports from jskelet. Client entries and islands may be .ts (esbuild strips types; manifest keys stay *.js). Server routes, hooks and jskelet.config.mjs stay JavaScript — use checkJs in jsconfig for editor checking there.
Each instance keeps its own L1 cache, so a cold boot still needs a warm-up and TTLs can drift. Early refresh and prewarm close most of that gap; an optional Redis tier shares HTML and broadcasts invalidateHtmlCache() / clearHtmlCache() to every replica over pub/sub. Targeted invalidation itself is first-class either way.
Yes. Without a manifest the asset helper falls back to unhashed paths and the layout simply prints no stylesheet tag. Forgetting the build step gives you an unstyled but working page, not an error.
More questions and the full mapping table live on the migration page.
One command scaffolds a working skeleton with a .jsk page, a component and an island.