How Instatic's Publisher Auto-Detects Dynamic Content for Hole Lazy-Loading
Instatic's publisher automatically identifies request-dependent content by traversing the page-tree in src/core/publisher/dynamicDetection.ts and applying four deterministic classification rules to determine which nodes must be rendered as <instatic-hole> placeholders for lazy-loading.
The Instatic static site generator (CoreBunch/Instatic) uses a two-phase publishing architecture where Phase A generates static shells at build time and Phase C inserts deferred placeholders for content that cannot be resolved until request time. The mechanism that powers this auto-detection for hole lazy-loading lives in a single source of truth: the findDynamicNodeIds function. This system walks the page-tree node by node, applying deterministic rules to decide exactly which components require lazy-loaded rendering.
The Four Auto-Detection Rules in dynamicDetection.ts
The detection logic resides in src/core/publisher/dynamicDetection.ts, where the classifyNode function implements four ordered rules (numbered 1-4 with an additional Rule 2b) to determine if a node must be deferred to request time. These rules are documented in the comment block at lines 14-26 of the file.
Rule 1: Explicitly Dynamic Modules
If the module definition in the registry has dynamic: true set, the node is immediately flagged as dynamic. This allows module authors to declare that a component is inherently request-dependent, bypassing further analysis.
Rule 2: Request-Dependent Bindings
A node becomes dynamic if it contains a dynamicBindings entry whose source is request-dependent. The helper function isBindingSourceRequestDependent encodes the built-in request-dependent sources (such as route.query.*) and can be extended via plugins.
Rule 2b extends this to string props containing interpolation tokens like {source.field}. If the token's source is request-dependent, the entire prop requires runtime interpolation, making the node dynamic.
Rule 3: Request-Dependent Loop Sources
Nodes of type base.loop are marked dynamic when their registered loop source is marked requestDependent: true or perVisitor: true in the loop registry. This ensures that iteration logic depending on the specific visitor or request context is deferred to Phase C.
Rule 4: Recursive Visual Component Analysis
If a node is a base.visual-component-ref, the detector recursively examines the visual component's definition tree. If any descendant within that tree is dynamic (with cycle detection to prevent infinite loops), the entire visual component reference is promoted to a hole. This ensures encapsulation of dynamic subtrees.
Page-Tree Traversal Logic
The findDynamicNodesWithReasons function orchestrates two distinct passes over the page-tree to ensure consistent classification while optimizing the number of holes generated.
Pre-Pass: Loop Promotion (Rule 3.5)
The first pass inspects static base.loop bodies (lines 107-122 in dynamicDetection.ts). If any descendant of a loop body is dynamic, the entire loop node is promoted to a hole, and the inner dynamic IDs are suppressed. This prevents emitting numerous individual holes for items within a dynamic list (addressing issue ISS-021) and keeps the shell HTML cleaner.
Main Pass: Node Classification
The second pass (lines 124-141) walks every page-node through the shared classifyNode function. When a node is classified as dynamic, its ID is added to the dynamicPageNodeIds set and a human-readable reason string is captured for diagnostics. Both passes use the same rule implementation, guaranteeing that the shell-vs-complete decision made during Phase A aligns perfectly with the placeholder emission in Phase C.
Public API for Dynamic Node Detection
The module exports two primary functions from src/core/publisher/index.ts for consumption by the rendering pipeline:
-
findDynamicNodeIds(page, site, registry): Returns aSetof page-node IDs that must be rendered as<instatic-hole>placeholders. This is the high-performance API used during publishing. -
findDynamicNodesWithReasons(page, site, registry): Returns an object containing both thedynamicPageNodeIdsset and areasonsarray of diagnostic strings explaining why each node was flagged. Use this for debugging and plugin development.
import {
findDynamicNodeIds,
findDynamicNodesWithReasons,
} from '@core/publisher'
// Standard usage during publish pass
const dynamicIds = findDynamicNodeIds(page, site, moduleRegistry)
// Diagnostic usage for debugging
const { dynamicPageNodeIds, reasons } = findDynamicNodesWithReasons(
page,
site,
moduleRegistry
)
console.log('Dynamic nodes:', [...dynamicPageNodeIds])
console.log('Classification reasons:', reasons)
Integration with the Rendering Pipeline
During the publish process in src/core/publisher/render.ts, the renderer first calls findDynamicNodeIds to obtain the complete set of hole IDs. As the tree-walking render function processes each node, it checks if the current node ID exists in that set:
function renderNode(nodeId: string) {
if (dynamicIds.has(nodeId)) {
return `<instatic-hole data-node-id="${nodeId}"></instatic-hole>`
}
// ...proceed with static markup generation...
}
When a match is found, the renderer emits an <instatic-hole> element with a data-node-id attribute instead of the node's actual markup. The client-side runtime subsequently fetches the missing fragment on demand when the page is served, completing the lazy-loading cycle.
Extending the Detection System
The architecture provides two primary extension points for customizing dynamic content detection:
Custom Binding Sources: Developers can extend isBindingSourceRequestDependent to recognize additional request-dependent sources beyond the built-in route.query and related fields. This function serves as a single, deliberate extensibility point for plugins.
Dynamic Module Registration: Modules can opt-in to dynamic detection by setting dynamic: true in their registry definition. This declarative approach ensures the detector automatically includes them without requiring custom traversal logic.
Summary
- Instatic's auto-detection for hole lazy-loading centers on the
findDynamicNodeIdsfunction insrc/core/publisher/dynamicDetection.ts. - Four ordered classification rules determine dynamism: explicit module flags, request-dependent bindings, request-dependent loop sources, and recursive visual component analysis.
- A two-pass traversal promotes entire loops to holes when their children are dynamic, preventing hole proliferation.
- The public API provides both production (
findDynamicNodeIds) and diagnostic (findDynamicNodesWithReasons) entrypoints. - The rendering pipeline uses these IDs to emit
<instatic-hole>placeholders, enabling client-side lazy-loading of request-dependent content.
Frequently Asked Questions
How does Instatic distinguish between static and dynamic content during publishing?
Instatic applies the classifyNode function from src/core/publisher/dynamicDetection.ts to every node in the page-tree. This function checks four specific rules: whether the module is explicitly marked dynamic, whether bindings or string tokens reference request-dependent sources, whether loops iterate over request-dependent data, and whether referenced visual components contain dynamic descendants. Nodes matching any rule are added to the dynamic set and rendered as holes.
What is the purpose of the <instatic-hole> placeholder?
The <instatic-hole> element serves as a server-side marker indicating where request-dependent content will be injected. During Phase A (build time), the publisher emits this placeholder instead of the actual component markup. When a visitor requests the page (Phase C), the runtime identifies these holes and fetches the missing fragments, enabling hole lazy-loading without blocking the initial static shell delivery.
Can plugins extend which binding sources are considered request-dependent?
Yes. The isBindingSourceRequestDependent function is designed as a single extensibility point. While the core system recognizes built-in sources like route.query, plugins can extend this function to recognize custom request-dependent sources. This allows the auto-detection rules to automatically flag nodes using these custom bindings without modifying the core classification logic.
Why does the detection system promote entire loops to holes instead of individual items?
The pre-pass traversal (lines 107-122 in dynamicDetection.ts) implements Rule 3.5 specifically to address performance concerns. If any descendant of a loop body is dynamic, the entire base.loop node is promoted to a single hole and the inner dynamic IDs are suppressed. This prevents emitting numerous small <instatic-hole> elements for each item in a dynamic list, reducing HTML size and client-side processing overhead (documented as issue ISS-021 in the codebase).
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 →