How Instatic's Three‑Layer Publishing System Delivers Sub‑Millisecond Response Times
Instatic eliminates per‑request rendering costs by serving pre‑baked HTML from disk, falling back to an in‑memory LRU cache, and hydrating only dynamic fragments on demand, achieving response times as low as 0.6 ms for static content.
The three‑layer publishing system in the CoreBunch/Instatic repository bridges the gap between static site speed and dynamic application flexibility. By combining atomic disk writes, intelligent in‑memory caching, and fine‑grained dynamic islands, the system ensures that fully static pages bypass the database entirely while still supporting personalized content where required.
Layer A: Disk‑Baked Artefacts for Zero‑Cost Serving
When a site is published, publishPage writes complete HTML files to uploads/published/current/<route>.html via staticArtefact.ts【/server/publish/staticArtefact.ts#L1-L9】. The write operation uses an atomic two‑slot symlink swap, ensuring readers never encounter a half‑written file.
The HTTP handler in publicRouter.ts checks this directory first. If the request URL contains only "junk" query parameters, the file streams directly from disk in approximately 0.6–1.4 ms with no database lookup, no template rendering, and no plugin execution【/server/publish/publicRouter.ts#L64-L68】. This represents the fastest path through the three‑layer publishing system.
Atomic Publishing Mechanics
The atomic swap mechanism guarantees consistency during deployments. While the new artefact writes to a staging slot, existing traffic continues reading from the current slot. Once the write completes, the symlink flips instantly, making the new version live without dropping connections or serving corrupted HTML.
Layer B: In‑Memory LRU Cache for Dynamic Routes
For routes containing request‑dependent data (loops, bindings, or pagination), renderCache.ts stores rendered HTML in an LRU cache keyed by (urlPath, canonicalQuery)【/server/publish/renderCache.ts#L1-L3】. The canonicalRenderQuery utility normalizes the key by stripping non‑essential parameters while preserving loop pagination values【/server/publish/loopPrefetch.ts#L60-L62】.
A cache hit returns the pre‑rendered body in approximately 0.8 ms without touching the database or re‑walking the page tree. When content changes, bumpPublishVersion() in publishState.ts evicts the entire cache, guaranteeing freshness and preventing stale data from persisting across publishes【/server/publish/publishState.ts#L49-L51】.
Layer C: Dynamic‑Island "Hole" Runtime for Lazy Hydration
Nodes that require request‑specific data are identified by findDynamicNodeIds in dynamicDetection.ts【/src/core/publisher/dynamicDetection.ts#L74-L86】. Instead of baking this content into the HTML, the publisher emits an <instatic‑hole> placeholder containing a minimal IntersectionObserver script (approximately 1.1 KB)【/server/publish/holeRuntime.ts#L50-L53】.
When the browser scrolls the hole into view, the client fetches the fragment from /_instatic/hole/<nodeId>, triggering a targeted server render for that subtree only. This keeps initial page loads fully static while deferring computation for truly dynamic content until it is actually needed.
Implementing the Three‑Layer Pipeline
The following examples demonstrate how these layers interact in practice.
Trigger the atomic bake process that populates Layer A:
import { publishDraftSite } from 'server/publish/publishSite';
// Writes static artefacts to disk and bumps publish version
await publishDraftSite(db, siteId);
The request handler implements the Layer A → Layer B → fallback cascade:
// Inside server/publish/publicRouter.ts
// Layer A: Attempt disk read first
const artefact = readArtefast(uploadsDir, url.pathname);
if (artefact) {
return streamFile(artefact); // ~0.6-1.4ms response
}
// Layer B: Check LRU cache for canonical query combinations
const cacheKey = canonicalRenderQuery(url);
const cached = renderCache.getOrRender(cacheKey, async () => {
const { html } = await publishPage(page, site, registry);
return { body: html };
});
if (cached) {
return sendResponse(cached.body); // ~0.8ms response
}
Mark components as dynamic to enable Layer C hole injection:
<div className="container">
<h1>Static Header</h1>
{/* Becomes <instatic-hole id="node-123"> at publish time */}
<DynamicWidget userId={user.id} />
</div>
When DynamicWidget exports dynamic: true from its module definition, publishPage in render.ts replaces its output with a hole placeholder. The holeRuntime.ts client script then manages lazy hydration as the user scrolls.
Summary
- Layer A (Disk Artefacts) delivers fully static pages in under 1.5 ms by streaming pre‑baked HTML directly from the filesystem, bypassing all application logic.
- Layer B (LRU Cache) serves semi‑dynamic pages in under 1 ms by storing rendered HTML in memory and invalidating atomically via version bumps.
- Layer C (Dynamic Islands) preserves static‑site performance for initial loads while supporting real‑time data through lazy‑loaded fragments fetched only when visible.
- Atomic Publishing ensures consistency during deployments through symlink swapping, preventing readers from seeing intermediate states.
Together, these layers give Instatic near‑static‑site speed while maintaining support for dynamic content where required.
Frequently Asked Questions
How does Instatic handle cache invalidation when content changes?
The bumpPublishVersion() function in server/publish/publishState.ts increments a global version counter whenever a publish completes【/server/publish/publishState.ts#L49-L51】. This version is captured at the start of each render, and any render that completes after a version change is discarded. Simultaneously, the LRU cache in renderCache.ts is cleared, ensuring subsequent requests receive freshly baked HTML.
What happens if a dynamic island is never scrolled into view?
The <instatic‑hole> element includes a lightweight IntersectionObserver that only triggers the fetch to /_instatic/hole/<nodeId> when the element enters the viewport【/server/publish/holeRuntime.ts#L50-L53】. If the user never scrolls to that section, the server never renders the fragment, saving both server CPU and network bandwidth.
How does the three‑layer system differentiate between query parameters?
Layer A serves requests with "junk" query parameters (UTM tags, tracking IDs) directly from disk, treating them as cacheable static requests. Layer B uses canonicalRenderQuery to normalize the cache key, stripping irrelevant parameters while preserving loop pagination values needed for dynamic content【/server/publish/loopPrefetch.ts#L60-L62】. This ensures pagination works correctly while maximizing cache hits.
Can personalized content be served from Layer A?
No. Layer A artefacts are completely static HTML files written during the publish process. Personalized content must flow through Layer C as dynamic islands identified by findDynamicNodeIds【/src/core/publisher/dynamicDetection.ts#L74-L86】. The publisher inserts hole placeholders for these nodes, and the personalization logic executes only when the browser requests the specific fragment endpoint.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →