Skip to content

JSkelet

Open source · Built for the modern web · Node.js 22+ · MIT licensed

The skeleton of the web.
Nothing more.

A minimal, high-performance foundation for content sites. Complete HTML from the server, islands only where you touch, cache ready before the visitor asks.

Get started
$ npx jskelet init
Minimal footprint Lightning fast Built for developers
JSkelet logo

Response

200 · HTML ready

Routes

route() · TTL

terminal

$ npx jskelet init

route ready

island ready

http://localhost:3000

45.0 kB Core payload (gzip)
0 Web fonts
22+ Node.js
MIT Open source
Express 5 Runtime

Why JSkelet

A framework that gets out of your way.

The server prepares the content, the cache removes the waiting, and islands add only the behaviour a page actually needs.

Minimal & Fast

Complete HTML from the server. No React runtime, no hydration waterfall — only the bytes a content page needs.

Modular

A menu, a counter or a chart lives in its own island. Its module downloads only when it becomes visible.

Developer First

Plain async controllers, EJS templates and a small API. Readable without ceremony, replaceable without drama.

Transparent performance

We don't claim lightweight. We show it.

These numbers are read from the real build output and reflect the weight after gzip. Working code, not a marketing promise.

This page's own build output, read from disk and measured as the request came in. The right column is after gzip. Island chunks download only when their element scrolls into view; a first load never fetches all of them.
Raw gzip
Stylesheet (Tailwind v4 output) 77.4 kB 13.1 kB
Client entry (island loader) 564 B 302 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

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

Write less. Build more.

Stale-while-revalidate keeps visitors out of the render queue. One route, one TTL, predictable behaviour.

  • The cache key is the path and query; on a hit the controller never runs.
  • Prewarm renders your pages at boot, so the first visitor never lands on a cold cache.
  • Every response carries its own cache status header, so measuring needs no extra tooling.
  • Targeted invalidation is built in: invalidateHtmlCache() stales a path, a pattern or a list without flushing everything.

View documentation

routes/10-pages.mjs
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

Redis, an admin panel, Cloudflare — without bolting on a second product.

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

Memory first. Redis when you scale out.

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

It doesn't try to do everything.

JSkelet is built for content and discoverability. For state-heavy applications, another tool is often the better answer.

Good fit

  • Content sites, blogs and documentation
  • Marketing and campaign pages
  • Product listings, catalogues and classifieds
  • Any page where SEO is a revenue line

Wrong choice

  • Visitor-facing dashboards and app UIs
  • Editor-like, state-heavy interfaces
  • Pages that change with every signed-in user
  • Real-time screens that update every second

FAQ

Frequently asked

Why is there no React?

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.

Can I use TypeScript?

The framework itself is plain JavaScript with JSDoc and has no compile step. On the application side you can enable checked JavaScript and keep most of the same type safety without a build; for .ts files you would add a step to the pipeline yourself.

The cache lives in process memory. What happens with multiple instances?

Each instance keeps its own L1 cache, so a cold boot still needs a warm-up and TTLs can drift. Prewarming closes 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.

Does it run without a build?

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.

Your first page is ready in five minutes.

One command scaffolds a working skeleton with a route, a component and an island.