HTML cache with stale-while-revalidate — ISR speed without a static rebuild
· 4 min read
Static site generation is fast until content changes. Every publish then means a rebuild, a deploy window, or an on-demand revalidation path that is easy to misconfigure and hard to reason about under load. Content teams feel that pain weekly: a copy fix sits in CI while social traffic already hits yesterday’s HTML.
JSkelet takes a different path. Controllers render complete HTML on the server. That HTML is stored in an in-memory cache keyed by path (and related variants), with a TTL. When an entry ages out, the runtime can still return the previous document immediately while a refresh runs in the background — classic stale-while-revalidate behaviour, without freezing the site at build time.
The HTML cache is a first-class part of the framework, not an add-on CDN trick you invent per project. Public route() handlers opt into revalidation windows; private surfaces stay out of the public keyspace.
Why content sites care more than dashboards
Brochure sites, docs, and blogs win on time-to-first-byte and readable markup, not on shipping a client framework to every landing page. An HTML cache preserves the “already rendered” advantage of SSG while editors stay in a normal CMS workflow: write, save, invalidate, move on.
Dashboards and signed-in tools are a different animal. Those routes should be marked private: true so personalised HTML never enters the public cache. Mixing the two models in one JSkelet process is fine; mixing them in one cache key is not.
For SEO and conversion pages, the cache is also a crawlability tool. Warm entries keep TTFB low. Complete HTML in the first response means crawlers and humans see the same document without waiting for a second render pass in the browser.
How a request flows through the cache
- A public GET arrives for a cacheable path.
- On a hit with remaining TTL, the stored HTML is returned as-is.
- On a miss, the controller runs: layout context, page data,
.jskrender, full document. - The response body is stored with a TTL and served to the caller.
- On soft expiry, the last good body can still be returned while a background refresh warms a new entry.
The important product detail is targeted invalidation. After an editor saves a post, you do not flush the entire site. You call invalidateHtmlCache for the list URL and that post’s URL (and any tag pages you care about). The next visitor warms fresh HTML; everyone else keeps reading the previous warm entry until their keys expire or get invalidated too.
Invalidation belongs next to the save handler in application code. If publish succeeds and the list still shows the old excerpt, you almost always forgot a path — not “the cache is broken.”
TTL, soft stale, and editorial expectations
Pick TTLs for how often content actually changes. A homepage that rarely moves can sit warm for an hour. A breaking-news surface wants shorter TTLs and aggressive invalidation. Blog posts sit in the middle: long TTL plus invalidate-on-publish is usually enough.
Editors should understand one sentence: saving does not always mean every visitor sees the change in the same millisecond, but invalidation makes the next render authoritative. That is the same mental model people already accept with CDN caches — only here the unit is full HTML documents inside the Node process (and optionally shared across replicas when you enable the shared cache path documented in JSkelet).
Failure modes worth designing for
- Stale list cards after publish → missing list invalidation.
- Private chrome in the public cache → a panel route without
private: true. - Store updated, HTML still old → keys not invalidated after mutation.
- Over-invalidation → every save flushes everything, and you recreate SSG-scale cold starts for no reason.
None of these require a new framework. They require naming the keys you care about and treating the HTML cache as part of the product surface JSkelet gives you.
Where ISR comparisons help — and where they mislead
ISR taught teams that “static-ish with revalidation” is a useful middle ground. The metaphor helps when explaining SWR to stakeholders. It misleads when people assume you still need a separate static pipeline, per-page build manifests, or a host-specific revalidation API as the only source of truth.
With JSkelet’s HTML cache, the source of truth is the render function plus your store. Revalidation is a property of the runtime. Hosting still matters for multi-instance sharing and edge delivery, but the editorial loop stays: mutate content → invalidate paths → next request warms HTML.
You get ISR-like speed characteristics without freezing marketing content at build time — and without teaching every editor how your CI pipeline works.
Comments
-
Alex Reader
September 19, 2026
Clear explanation of stale-while-revalidate in JSkelet — thanks.