Skip to content
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
JSkelet as a thin layer between your content and the web
46.5 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.

01

Minimal & Fast

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

02

Modular

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

03

Developer First

Plain async controllers, .jsk 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) 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

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 — add cache().vary.host when locale lives on the Host, so one language never leaks onto another.
  • Fresh entries revalidate in the background before TTL expiry; idle ones soft-stale and drain over HTTP even without a classic prewarm list.
  • Prewarm at boot from a path list, or onVisit from same-origin links after each public response — pick one mode, not both.
  • 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.

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.

Good fit

  • Content sites, blogs and documentation
  • Marketing and campaign pages
  • Product listings, catalogues and classifieds
  • Any page where SEO is a revenue line
  • Signed-in dashboards and per-visitor panels (private: true)
  • Form-driven UIs with fragments and region swaps

Poor fit

  • Full SPA shells and client-side app routers
  • Editor-like, collaborative, state-heavy client trees
  • Streaming SSR or React Server Components
  • Built-in real-time push (SSE, WebSocket or polling is yours)

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 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.

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. 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.

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 .jsk page, a component and an island.