Slow Rendering Jay-Html To Jay-Html
Slow Rendering: Jay-HTML to Jay-HTML
Written for AI agents. See Log Methodology Note below for details.
Summary
Jay-html to jay-html slow rendering pre-renders slow-phase bindings at build time, producing a new jay-html file with static data "baked in" while preserving fast/interactive bindings. This enables faster request-time rendering by skipping slow-phase work.
Key features:
- Text/attribute bindings with
phase: sloware resolved to literal values - Slow conditionals (
if) are evaluated and removed or stripped - Slow arrays (
forEach) are unrolled toslowForEachelements withjayIndexandjayTrackByattributes - Fast/interactive bindings remain as template syntax for runtime resolution
- Runtime
slowForEachItem()function provides correct data context for item-scoped bindings
Status: Core transformation engine, runtime support, component handling, and dev server integration implemented (Phase 1-5). Production build pending (Phase 6-7).
Background
Jay Stack has three rendering phases (see Design Logs #34, #50):
- Slow (build-time): Static data rendered at build time or data-change time
- Fast (request-time): Dynamic data rendered per HTTP request (SSR)
- Interactive (client-side): Client-side interactivity and state management
Currently, all three phases execute at runtime:
- The dev server runs slow → fast → interactive on each request
- Production could pre-render at build time, but the mechanism isn't defined
This design log proposes a jay-html to jay-html transformation for the slow phase:
- Input: Original
*.jay-htmlfile with bindings for all phases - Output: Pre-rendered
*.jay-htmlfile with slow data "baked in"
The generated jay-html can then be used for faster rendering in both dev server and production.
Problem Statement
Current Behavior
Every page request runs the full rendering pipeline:
Request → Load jay-html → Run slow render → Run fast render → Send HTML
↓
(same work repeated every time)
For pages with slow-phase data (static product info, categories, etc.), this is wasteful.
Desired Behavior
Pre-render slow data once, reuse for every request:
Build Time:
jay-html → slow render → pre-rendered jay-html (with slow data embedded)
Request Time:
pre-rendered jay-html → fast render → Send HTML
↓
(skip slow render)
Use Cases
- Production builds: Generate pre-rendered jay-html files during build
- Dev server: Generate on first request, watch for changes, regenerate as needed
- Incremental regeneration: When slow data changes, regenerate specific pages
Questions & Answers
Q1: What exactly gets "baked in" to the output jay-html?
A: Properties marked as phase: slow in the contract:
- Text content:
{productName}→ literal value - Attributes:
href="{productUrl}"→ literal value - Conditionals:
if="isOnSale"→ either present or removed - Loops:
forEach="images"→ unrolled to concrete elements
Q2: How do we identify which bindings are slow vs fast/interactive?
A: From the contract's phase annotations (Design Log #50):
contract ProductPage
- {tag: name, type: data, dataType: string, phase: slow}
- {tag: price, type: data, dataType: number, phase: fast}
- {tag: quantity, type: variant, dataType: number, phase: fast+interactive}
The compiler already generates SlowViewState, FastViewState, and InteractiveViewState types.
Q3: How do we handle arrays (forEach) in slow phase?
A: Two scenarios:
Slow array (structure frozen at build time):
<!-- Input -->
<ul>
<li forEach="images" trackBy="id">
<img src="{url}" alt="{alt}" />
</li>
</ul>
<!-- Output (unrolled) -->
<ul>
<li data-track-by="img1">
<img src="/hero.jpg" alt="Hero image" />
</li>
<li data-track-by="img2">
<img src="/detail.jpg" alt="Detail view" />
</li>
</ul>
Fast array (structure set at request time):
The forEach is preserved, only child properties with phase: slow are baked in.
Q4: What about conditionals (if) in slow phase?
A:
Slow conditional:
<!-- Input -->
<span class="badge" if="isOnSale">On Sale!</span>
<!-- Output (condition evaluated) -->
<span class="badge">On Sale!</span>
<!-- or element removed if false -->
Fast conditional:
The if attribute is preserved for runtime evaluation.
Q5: How do we handle nested components (headless/headful)?
A:
Headful components: Each component's jay-html is transformed separately. The component reference stays, but the referenced jay-html is pre-rendered.
Headless components: The component's slot content may have slow bindings that get baked in. The component structure itself is preserved.
Q6: What's the output file naming/location?
A: With loadParams, a single jay-html can generate multiple pre-rendered files - one for each combination of URL parameters.
Example: /products/[slug]/page.jay-html with:
loadParams: async () => [{ slug: 'widget-a' }, { slug: 'widget-b' }, { slug: 'gadget-pro' }];
Generates:
.jay/pre-rendered/
products/
widget-a/
page.jay-html
widget-b/
page.jay-html
gadget-pro/
page.jay-html
Storage Structure:
.jay/pre-rendered/
<resolved-route-path>/
page.jay-html
metadata.json # { params, renderedAt, sourceHash }
Options:
- Production builds:
.jay/pre-rendered/directory with param-resolved paths - Dev server: In-memory cache keyed by
(jayHtmlPath, params)
Recommendation:
- Production: File-based storage in
.jay/pre-rendered/with resolved paths - Dev server: In-memory cache, no file writes
Q7: How do we preserve fast/interactive bindings in the output?
A: Bindings for fast/interactive phases remain as template syntax:
<!-- Input -->
<div>
<h1>{productName}</h1>
<!-- slow -->
<span class="price">{price}</span>
<!-- fast -->
<button>{addToCartText}</button>
<!-- interactive -->
</div>
<!-- Output (slow rendered) -->
<div>
<h1>Awesome Widget</h1>
<!-- baked in -->
<span class="price">{price}</span>
<!-- preserved -->
<button>{addToCartText}</button>
<!-- preserved -->
</div>
Q8: What about CSS/style bindings?
A: Same rules apply:
<!-- Input -->
<div style:background-color="{bgColor}">
<!-- slow -->
<!-- Output -->
<div style="background-color: #ff0000;"><!-- baked in --></div>
</div>
Q9: How do we handle refs in slow-rendered elements?
A: Refs are preserved - they're for interactive phase only:
<!-- Input -->
<button ref="addToCart">{buttonText}</button>
<!-- Output -->
<button ref="addToCart">Add to Cart</button>
<!-- ref preserved, text baked in -->
Q10: What happens when slow data changes?
A: Different strategies for different environments:
Dev server:
- No need to watch data sources - simplifies dev experience
- Watch jay-html and component files - invalidate cache when template changes
- On file change: Clear cached pre-render, regenerate on next request
- Developers can manually refresh to get fresh slow data
Production builds:
- Full rebuild regenerates all pre-rendered files
- Incremental rebuild possible if dependency tracking is implemented
Production runtime (ISR-like):
- Webhook/API to trigger regeneration of specific pages
- Time-based revalidation (optional, configurable per page)
Q11: How does this interact with the existing dev server?
A:
Current dev server flow:
Request → Vite loads jay-html → Run slow render → Run fast render → Respond
New dev server flow:
First Request (for given params):
jay-html + params → slow render → cache pre-rendered jay-html (keyed by path+params)
Subsequent Requests (same params):
cached pre-rendered jay-html → fast render → Respond
On jay-html/Component File Change:
Invalidate affected cache entries → regenerate on next request
On Data Source Change:
(no automatic invalidation - developer refreshes manually)
Cache Key: (jayHtmlPath, params) tuple - each param combination cached separately
Q12: What metadata needs to be preserved in the output?
A:
- Contract reference (for fast/interactive type checking)
- Phase information (which bindings are fast vs interactive)
- Track-by keys for arrays (for client-side reconciliation)
- Refs for interactive elements
- Component imports (for nested components)
Design
Core Transformation
The slow-render transformation takes:
- Input: Original jay-html + SlowViewState data
- Output: Pre-rendered jay-html with slow bindings resolved
interface SlowRenderInput {
jayHtmlPath: string;
slowViewState: object;
contract: ContractMetadata;
}
interface SlowRenderOutput {
preRenderedJayHtml: string;
}
Transformation Rules
Rule 1: Text Bindings
<!-- Slow binding -->
<span>{productName}</span>
<!-- → -->
<span>Awesome Widget</span>
<!-- Fast binding (preserved) -->
<span>{price}</span>
<!-- → -->
<span>{price}</span>
Rule 2: Attribute Bindings
<!-- Slow binding -->
<a href="{productUrl}">Link</a>
<!-- → -->
<a href="/products/awesome-widget">Link</a>
<!-- Fast binding (preserved) -->
<span class="{priceClass}">...</span>
<!-- → -->
<span class="{priceClass}">...</span>
Rule 3: Style Bindings
<!-- Slow binding -->
<div style:background-color="{bgColor}">
<!-- → -->
<div style="background-color: #ff0000;">
<!-- Fast binding (preserved) -->
<div style:opacity="{fadeLevel}">
<!-- → -->
<div style:opacity="{fadeLevel}"></div>
</div>
</div>
</div>
Rule 4: Conditionals
<!-- Slow condition (evaluated) -->
<span if="isOnSale">On Sale!</span>
<!-- → if true: -->
<span>On Sale!</span>
<!-- → if false: (element removed) -->
<!-- Fast condition (preserved) -->
<span if="inStock">In Stock</span>
<!-- → -->
<span if="inStock">In Stock</span>
Rule 5: Loops (forEach)
<!-- Slow array (unrolled, forEach replaced with slowForEach) -->
<li forEach="images" trackBy="id">
<img src="{url}" alt="{alt}" />
</li>
<!-- → -->
<li slowForEach="images" trackBy="id" jayIndex="0" jayTrackBy="img1">
<img src="/hero.jpg" alt="Hero image" />
</li>
<li slowForEach="images" trackBy="id" jayIndex="1" jayTrackBy="img2">
<img src="/detail.jpg" alt="Detail view" />
</li>
<!-- Fast array (preserved) -->
<li forEach="products" trackBy="id">
<span>{name}</span>
</li>
<!-- → (unchanged) -->
<li forEach="products" trackBy="id">
<span>{name}</span>
</li>
Attribute semantics:
slowForEach="arrayName"- indicates this is an unrolled slow array item (replacesforEach)trackBy="propertyName"- preserved from original (property name used for tracking)jayIndex="N"- index of this item in the arrayjayTrackBy="value"- the actual track-by value for this item
Important: Slow arrays preserve metadata via attributes so that:
- Fast rendering can match fast properties to the correct array items
- Interactive rendering can identify items for updates
- Client-side reconciliation works correctly
Rule 6: Mixed Phase in Arrays
When a slow array has mixed-phase child properties:
<!-- Slow array with fast child properties -->
<li forEach="products" trackBy="id">
<span>{name}</span>
<!-- slow: baked in -->
<span>{price}</span>
<!-- fast: preserved as binding -->
</li>
<!-- → (unrolled with metadata, bindings remain item-scoped) -->
<li slowForEach="products" trackBy="id" jayIndex="0" jayTrackBy="prod1">
<span>Widget A</span>
<span>{price}</span>
</li>
<li slowForEach="products" trackBy="id" jayIndex="1" jayTrackBy="prod2">
<span>Widget B</span>
<span>{price}</span>
</li>
Key points:
forEachreplaced withslowForEachto indicate pre-rendered array- Fast bindings remain item-scoped (
{price}not{products[0].price}) slowForEachItemruntime function changes context to the array itemjayIndextells the runtime which item to use from the fast ViewState array
Rule 7: Recursive Regions
For recursive structures (see Design Log #46):
<div ref="treeNode">
<span>{name}</span>
<!-- slow: baked in -->
<ul if="open">
<!-- could be slow or fast -->
<li forEach="children" trackBy="id">
<recurse ref="treeNode" />
</li>
</ul>
</div>
<!-- If entire structure is slow → fully unrolled -->
<!-- If children array is fast → recursion preserved -->
Output Jay-HTML Format
The output must be a valid jay-html that can be processed by the fast/interactive phases.
<html>
<head>
<!-- Contract reference preserved -->
<script type="application/jay-contract" src="./page.jay-contract"></script>
<!-- Component imports preserved -->
<script type="application/jay-headless" src="@wix/stores" names="ProductCard"></script>
</head>
<body>
<!-- Pre-rendered content with fast/interactive bindings -->
<h1>Awesome Widget</h1>
<span class="price">{price}</span>
<button ref="addToCart">{addToCartText}</button>
</body>
</html>
Type Generation for Pre-Rendered Jay-HTML
The pre-rendered jay-html generates types for remaining phases:
// Original: All three phase types
interface ProductPageViewState {
name;
sku;
price;
quantity;
}
interface ProductPageSlowViewState {
name;
sku;
}
interface ProductPageFastViewState {
price;
}
interface ProductPageInteractiveViewState {
quantity;
}
// Pre-rendered: Only fast + interactive
interface ProductPagePreRenderedViewState {
price;
quantity;
}
interface ProductPagePreRenderedFastViewState {
price;
}
interface ProductPagePreRenderedInteractiveViewState {
quantity;
}
Integration Points
1. Build-Time Pre-Rendering
// In build script
async function preRenderPage(pagePath: string) {
const component = await loadComponent(pagePath);
const slowViewState = await component.withSlowlyRender(props, services);
const preRenderedJayHtml = await slowRenderJayHtml({
jayHtmlPath: pagePath,
slowViewState: slowViewState.render,
contract: component.contract,
});
await writeFile(pagePath.replace('.jay-html', '.pre-rendered.jay-html'), preRenderedJayHtml);
}
2. Dev Server Integration
// Cache key includes params for multi-param pages
type CacheKey = `${string}:${string}`; // `jayHtmlPath:JSON.stringify(params)`
const preRenderCache = new Map<CacheKey, PreRenderedJayHtml>();
async function getJayHtml(pagePath: string, params: Record<string, string>) {
const cacheKey: CacheKey = `${pagePath}:${JSON.stringify(params)}`;
if (preRenderCache.has(cacheKey)) {
return preRenderCache.get(cacheKey);
}
const preRendered = await preRenderPage(pagePath, params);
preRenderCache.set(cacheKey, preRendered);
return preRendered;
}
// Watch jay-html and component files (NOT data sources)
watcher.on('change', (changedPath) => {
// Invalidate all cached pre-renders that depend on the changed file
const affectedPages = findDependentPages(changedPath);
for (const pagePath of affectedPages) {
// Clear all param variants of this page
for (const key of preRenderCache.keys()) {
if (key.startsWith(`${pagePath}:`)) {
preRenderCache.delete(key);
}
}
}
});
// Note: No watching of data sources - developers refresh to get fresh slow data
3. Request-Time Rendering
// Fast render uses pre-rendered jay-html
async function handleRequest(req: Request) {
const preRenderedJayHtml = await getJayHtml(route.pagePath);
// Fast render only needs to fill in fast-phase bindings
const html = await fastRender(preRenderedJayHtml, fastViewState);
return html;
}
Implementation Plan
Phase 1: Core Transformation Engine
- Create
slow-render-transform.tsin compiler - Implement binding resolution for each rule (text, attr, style, if, forEach)
- Handle phase detection from contract metadata
- Generate valid jay-html output
Tests:
- Text binding resolution (slow → literal, fast → preserved)
- Attribute binding resolution
- Style binding resolution
- Conditional evaluation (slow conditions resolved, fast preserved)
- Basic forEach → slowForEach transformation
- Mixed bindings in same element
Phase 2: Array Handling
- Implement slow array unrolling with
slowForEachattribute - Handle mixed-phase arrays (slow structure, fast properties)
- Preserve trackBy and add jayIndex, jayTrackBy attributes
- Keep bindings item-scoped (no rewriting to indexed access)
Tests:
- Pure slow array unrolling
- Mixed-phase array (slow structure, fast child properties)
- Nested arrays
- Array with conditionals inside
- trackBy value extraction
Phase 3: Runtime Support for slowForEach
- Update jay-html parser to recognize
slowForEachattribute - Implement
slowForEachItemruntime function that:- Uses
jayIndexto accessviewState[arrayName][index] - Sets up data context for child bindings (like
forEach) - Provides correct item context for events
- Uses
- Ensure client reconciliation works with jayTrackBy
Tests:
- Fast render with slowForEach elements
- Context switching (bindings use item scope)
- Interactive updates to fast properties in slow arrays
- Event handling with correct item context
- Client hydration with slowForEach
Phase 4: Component Handling
- Handle nested component references
- Recursive region support
- Headless component slot content
Phase 5: Dev Server Integration
- Add pre-render cache to dev server
- Integrate with file watcher for invalidation (jay-html and component files)
- Lazy pre-render on first request
- Cache keyed by (jayHtmlPath, params)
Phase 6: Production Build (Future)
Note: Production build not yet supported. Skip for now.
- Pre-render all pages that have slow phase data (no configuration needed)
- Generate pre-rendered jay-html files in
.jay/pre-rendered/ - Handle loadParams to generate multiple files per route
Phase 7: Incremental Regeneration (Future)
- API/webhook for triggering regeneration
- Dependency tracking (which pages depend on which data)
- Partial regeneration support
Examples
Example 1: Product Page
Input (page.jay-html):
<html>
<head>
<script type="application/jay-contract" src="./page.jay-contract"></script>
</head>
<body>
<article class="product">
<h1>{name}</h1>
<p class="sku">SKU: {sku}</p>
<span class="price">{formattedPrice}</span>
<span class="stock" if="inStock">In Stock</span>
<div class="quantity">
<button ref="decrease">-</button>
<span>{quantity}</span>
<button ref="increase">+</button>
</div>
<ul class="images">
<li forEach="images" trackBy="id">
<img src="{url}" alt="{alt}" />
</li>
</ul>
</article>
</body>
</html>
Contract (page.jay-contract):
contract ProductPage
tags:
- {tag: name, type: data, dataType: string, phase: slow}
- {tag: sku, type: data, dataType: string, phase: slow}
- {tag: formattedPrice, type: data, dataType: string, phase: fast}
- {tag: inStock, type: data, dataType: boolean, phase: fast}
- {tag: quantity, type: variant, dataType: number, phase: fast+interactive}
- {tag: images, type: repeated, phase: slow}
- {tag: id, type: data, dataType: string}
- {tag: url, type: data, dataType: string}
- {tag: alt, type: data, dataType: string}
SlowViewState:
{
name: "Awesome Widget",
sku: "AW-12345",
images: [
{ id: "1", url: "/images/hero.jpg", alt: "Hero shot" },
{ id: "2", url: "/images/detail.jpg", alt: "Detail view" }
]
}
Output (page.pre-rendered.jay-html):
<html>
<head>
<script type="application/jay-contract" src="./page.jay-contract"></script>
</head>
<body>
<article class="product">
<h1>Awesome Widget</h1>
<p class="sku">SKU: AW-12345</p>
<span class="price">{formattedPrice}</span>
<span class="stock" if="inStock">In Stock</span>
<div class="quantity">
<button ref="decrease">-</button>
<span>{quantity}</span>
<button ref="increase">+</button>
</div>
<ul class="images">
<li slowForEach="images" trackBy="id" jayIndex="0" jayTrackBy="1">
<img src="/images/hero.jpg" alt="Hero shot" />
</li>
<li slowForEach="images" trackBy="id" jayIndex="1" jayTrackBy="2">
<img src="/images/detail.jpg" alt="Detail view" />
</li>
</ul>
</article>
</body>
</html>
Example 2: Category Page with Mixed-Phase Array
Input:
<ul class="products">
<li forEach="products" trackBy="id">
<span class="name">{name}</span>
<!-- slow -->
<span class="price">{price}</span>
<!-- fast -->
<button if="inStock">Add</button>
<!-- fast condition -->
</li>
</ul>
Contract: products array is phase: slow but price and inStock are phase: fast
Output:
<ul class="products">
<li slowForEach="products" trackBy="id" jayIndex="0" jayTrackBy="prod1">
<span class="name">Widget A</span>
<span class="price">{price}</span>
<button if="inStock">Add</button>
</li>
<li slowForEach="products" trackBy="id" jayIndex="1" jayTrackBy="prod2">
<span class="name">Widget B</span>
<span class="price">{price}</span>
<button if="inStock">Add</button>
</li>
</ul>
Key points:
slowForEach="products"+jayIndextell the runtime which array item to use- Bindings remain item-scoped (
{price}not{products[0].price}) - Runtime's
slowForEachItemchanges context to the correct array item
TypeScript Code Generation
This section shows how pre-rendered jay-html compiles to TypeScript, validating that the runtime can handle the new constructs.
Key Insight: Context Switching
slowForEachItem works like forEach by changing the data context to the array item. This means:
- Bindings inside use item-scoped access (
vs.price) not indexed access (vs.products[0].price) - Events get the correct item coordinates and data
- The transformation is simpler - no need to rewrite bindings with indexes
- Consistent with how
forEachworks
Example: slowForEach Compilation
Pre-rendered jay-html:
<ul class="products">
<li slowForEach="products" trackBy="id" jayIndex="0" jayTrackBy="prod1">
<span class="name">Widget A</span>
<span class="price">{price}</span>
</li>
<li slowForEach="products" trackBy="id" jayIndex="1" jayTrackBy="prod2">
<span class="name">Widget B</span>
<span class="price">{price}</span>
</li>
</ul>
Generated TypeScript:
import { element as e, dynamicText as dt, slowForEachItem } from '@jay-framework/runtime';
// FastViewState item type (only fast/interactive fields)
interface ProductFastItem {
price: number;
inStock: boolean;
}
export function render() {
return e('ul', { class: 'products' }, [
// slowForEachItem: changes context to vs.products[0]
slowForEachItem(
'products', // array name for context lookup
0, // jayIndex - which item in the array
'prod1', // jayTrackBy value
() =>
e('li', {}, [
// elementCreator function
e('span', { class: 'name' }, ['Widget A']), // static text (pre-rendered)
e('span', { class: 'price' }, [
dt((vs: ProductFastItem) => vs.price), // item-scoped binding (not indexed!)
]),
]),
),
slowForEachItem('products', 1, 'prod2', () =>
e('li', {}, [
e('span', { class: 'name' }, ['Widget B']),
e('span', { class: 'price' }, [
dt((vs: ProductFastItem) => vs.price), // same binding, different context
]),
]),
),
]);
}
How it works:
slowForEachItem('products', 0, ...)sets the data context toviewState.products[0]- Inside the item, bindings use
vs.price(item-scoped, like in regularforEach) - The runtime's update function knows to get
viewState.products[0]and pass it to child bindings - Events fire with the correct array item context
Example: Mixed Bindings with Conditionals
Pre-rendered jay-html:
<li slowForEach="products" trackBy="id" jayIndex="0" jayTrackBy="prod1">
<span class="name">Widget A</span>
<span class="price">{price}</span>
<button if="inStock">Add to Cart</button>
</li>
Generated TypeScript:
slowForEachItem('products', 0, 'prod1', () =>
e('li', {}, [
e('span', { class: 'name' }, ['Widget A']),
e('span', { class: 'price' }, [
dt((vs) => vs.price), // item-scoped
]),
conditional(
(vs) => vs.inStock, // item-scoped conditional
() => e('button', {}, ['Add to Cart']),
),
]),
);
Example: Pure Slow Array (No Fast Properties)
When all array item properties are slow:
Pre-rendered jay-html:
<ul class="images">
<li slowForEach="images" trackBy="id" jayIndex="0" jayTrackBy="1">
<img src="/hero.jpg" alt="Hero shot" />
</li>
<li slowForEach="images" trackBy="id" jayIndex="1" jayTrackBy="2">
<img src="/detail.jpg" alt="Detail view" />
</li>
</ul>
Generated TypeScript:
e('ul', { class: 'images' }, [
slowForEachItem('images', 0, '1', () =>
e('li', {}, [
e('img', { src: '/hero.jpg', alt: 'Hero shot' }), // fully static
]),
),
slowForEachItem('images', 1, '2', () =>
e('li', {}, [e('img', { src: '/detail.jpg', alt: 'Detail view' })]),
),
]);
Note: Even with no fast bindings, slowForEachItem sets up the context for events and potential interactive updates. The element is wrapped in a function so it's constructed within the correct data context.
Runtime Function Signature
/**
* Wraps a pre-rendered array item from slow phase.
* Sets the data context to viewState[arrayName][index] for child bindings.
*
* @param arrayName - Name of the source array in parent ViewState
* @param index - Index of this item (used to access viewState[arrayName][index])
* @param trackByValue - The track-by value for client reconciliation
* @param elementCreator - Function that creates the element (called within item context)
*/
function slowForEachItem<ParentVS, ItemVS>(
arrayName: keyof ParentVS,
index: number,
trackByValue: string,
elementCreator: () => BaseJayElement<ItemVS>,
): BaseJayElement<ParentVS>;
Note: The elementCreator is a function (not a direct element) because it must be invoked within the item's data context. This ensures bindings resolve against the correct array item during construction.
Comparison: forEach vs slowForEachItem
| Aspect | forEach |
slowForEachItem |
|---|---|---|
| Structure | Dynamic (rendered at runtime) | Static (pre-rendered) |
| Item count | Determined at runtime | Fixed at slow-render time |
| Context switching | Yes | Yes |
| Bindings | Item-scoped (vs.price) |
Item-scoped (vs.price) |
| Track-by | Computed at runtime | Baked in as attribute |
| Add/remove items | Supported | Not supported (frozen structure) |
Trade-offs
Advantages
- Performance: Skip slow render on every request
- Caching: Pre-rendered jay-html is highly cacheable
- Consistency: Same pre-render output used across dev and production
- Debugging: Pre-rendered files can be inspected
Disadvantages
- Complexity: Additional transformation step
- Staleness: Pre-rendered data can become stale
- Build time: Initial build is slower
- Mixed-phase arrays: Complex handling for fast bindings in slow arrays
Alternatives Considered
Full HTML pre-rendering: Pre-render to final HTML
- Rejected: Loses flexibility for fast-phase rendering
Virtual DOM approach: Keep structure in memory
- Rejected: Doesn't persist across server restarts
JSON snapshot: Store slow data as JSON, apply at runtime
- Rejected: Still requires runtime template processing
Verification Criteria
- Pre-rendered jay-html is valid and parseable
- Fast/interactive bindings are preserved correctly
- Unrolled arrays maintain track-by keys
- Dev server correctly invalidates cache on changes
- Production build generates correct pre-rendered files
- Type generation works for pre-rendered jay-html
- Performance improvement is measurable (skip slow render)
Resolved Questions
Partial pre-rendering: No - all slow bindings are pre-rendered together.
CDN integration: Not applicable - pre-rendered jay-html files are not fully rendered pages and cannot be served directly from CDN. Future consideration: complete rendering at the edge.
Syntax for unrolled arrays: Use
slowForEachattribute instead of wrapping elements. This is consistent with the existingforEachattribute syntax:forEach="array"→slowForEach="array"(indicates pre-rendered slow array)- Same
trackByattribute preserved - Added
jayIndexandjayTrackByattributes for item identification
Fast ViewState array structure: Fast ViewState only includes fast/interactive phase fields, consistent with the phase-specific type generation (Design Log #50). For unrolled slow arrays with mixed phases:
// SlowViewState (already rendered, baked into jay-html) { products: [{ name: 'Widget A' }, { name: 'Widget B' }]; } // FastViewState (provided at request time) { products: [ { price: 29.99, inStock: true }, { price: 19.99, inStock: false }, ]; }The
slowForEachItemruntime function usesjayIndexto access the correct item from the fast ViewState array and sets up the data context. Bindings remain item-scoped (e.g.,{price}not{products[0].price}).
Related Design Logs
- #34 - Jay Stack: Original architecture and rendering phases
- #50 - Rendering Phases in Contracts: Phase annotations in contracts
- #52 - Client-Server Code Splitting: Separating client/server code
- #46 - Recursive Jay-HTML: Recursive region handling
Implementation Results
Phase 1-3: Core Transformation and Runtime Support (COMPLETED)
Implemented:
Core Transformation Engine (
compiler-jay-html/lib/slow-render/slow-render-transform.ts)slowRenderTransform()function for jay-html to jay-html transformationhasSlowPhaseProperties()utility to check if pre-rendering is applicable- Phase detection from contract metadata
- Text binding resolution for slow-phase properties
- Attribute binding resolution
- Conditional (if) handling - removes false slow conditions, strips if attr from true ones
Array Unrolling (
forEach→slowForEach)- Slow arrays are unrolled to individual
slowForEachelements - Each item gets
jayIndex,jayTrackByattributes - Mixed-phase arrays: slow bindings resolved, fast bindings preserved as item-scoped
- Slow arrays are unrolled to individual
Compiler Integration
- Added
isSlowForEach()andgetSlowForEachInfo()helpers - Compiler generates
slowForEachItem()calls for pre-rendered arrays - Proper imports for
slowForEachItemfrom runtime - Skip slowForEach attributes in regular attribute rendering
- Added
Runtime Support (
runtime/lib/element.ts)slowForEachItem<ParentVS, ItemVS>()function- Sets data context to the correct array item
- Enables fast/interactive updates within pre-rendered items
- Context-aware update that resolves item from parent ViewState
Tests:
- 18 tests for slow-render-transform (all passing)
- 10 tests for slowForEachItem runtime (all passing)
- 1 test for slowForEach compilation (all passing)
- Total: 427 compiler tests passing, 181 runtime tests passing
Files Created/Modified:
- NEW:
compiler-jay-html/lib/slow-render/slow-render-transform.ts - NEW:
compiler-jay-html/test/slow-render/slow-render-transform.test.ts - NEW:
compiler-jay-html/test/fixtures/slow-render/(12 fixture directories with input/output/contract files) - NEW:
runtime/runtime/test/lib/slow-for-each-item.test.ts - NEW:
compiler-jay-html/test/fixtures/collections/slow-for-each/ - MODIFIED:
compiler-jay-html/lib/jay-target/jay-html-helpers.ts - MODIFIED:
compiler-jay-html/lib/jay-target/jay-html-compiler.ts - MODIFIED:
compiler-jay-html/lib/index.ts - MODIFIED:
compiler-shared/lib/imports.ts - MODIFIED:
runtime/runtime/lib/element.ts
Deviations from Original Design
Removed pre-render metadata script: The design included an
application/jay-prerender-metascript in the output head withrenderedAtandslowProperties. This was removed as unnecessary - theslowForEach/jayIndex/jayTrackByattributes provide sufficient information for runtime.SlowRenderInput uses content instead of path:
- Design:
jayHtmlPath: string - Implementation:
jayHtmlContent: string - Rationale: More flexible - caller controls file loading
- Design:
slowForEachItem takes elementCreator function:
- Design:
element: JayElement<ItemVS>(direct element) - Implementation:
elementCreator: () => BaseJayElement<ItemVS>(function) - Rationale: The element must be constructed within the item context for bindings to resolve correctly. Passing a pre-constructed element would use the wrong context.
- Design:
Style binding syntax differs from design: The design showed
style:background-color="{bgColor}"syntax, but jay-html usesstyle="background-color: {bgColor}"(bindings embedded in the style attribute value). The implementation correctly handles this actual syntax.Recursive regions preserved, not unrolled: The design mentioned full unrolling for slow recursive structures. The implementation preserves
<recurse>elements as-is, deferring recursive evaluation to runtime. This simplifies implementation and handles the common case where recursive data is fast/interactive phase.
Phase 4: Component and Recursive Handling (COMPLETED)
Implemented:
Recursive regions preserved:
<recurse ref="..."/>elements pass through the transformation unchanged. If the recursive data is fast phase, the recursion is evaluated at runtime.Headless component references preserved:
<script type="application/jay-headless">in head section is preserved. The transformation only modifies body content.Slow bindings within components resolved: Text/attribute bindings with slow phase data are still resolved within component templates.
Tests added:
recursive-preserved- verifies recursive regions with fast data are unchangedheadless-preserved- verifies headless imports are preserved and slow bindings resolved
Phase 5: Dev Server Integration (COMPLETED)
Key Benefit: Since slow ViewState is baked directly into the pre-rendered jay-html, we don't need to pass it to the client. The client only receives fast and interactive ViewState, reducing payload size and simplifying the client-side code.
Current flow (without pre-rendering):
Request → loadPageParts → runSlowlyForPage → renderFastChangingData → generateClientScript
↓
(slow ViewState sent to client)
New flow (with pre-rendering, dontCacheSlowly: false):
First Request (cache miss):
loadPageParts → runSlowlyForPage → transform jay-html → cache(preRenderedHtml + carryForward) → renderFast
Subsequent Requests (cache hit):
check cache → loadPageParts(preRenderedHtml) → renderFast(cached carryForward)
↓ ↓
(skip slow rendering) (only fast+interactive ViewState to client)
Legacy flow (when dontCacheSlowly: true):
Request → loadPageParts → runSlowlyForPage → renderFastChangingData → generateClientScript
↓
(full slow+fast ViewState sent to client)
Implemented:
SlowRenderCache class (
stack-server-runtime/lib/slow-render-cache.ts)- Disk-based caching: Pre-rendered jay-html files are written to
<buildFolder>/slow-render-cache/ - Cache keyed by
(jayHtmlPath, params)with MD5 hash for params in filename - Caches
preRenderedPath(file path),carryForward, andslowViewState - Async
set()returns the path where the file was written - Async
invalidate()deletes cached files from disk pathToKeysmap for efficient invalidation of all param variants
- Disk-based caching: Pre-rendered jay-html files are written to
LoadPageParts enhancement (
stack-server-runtime/lib/load-page-parts.ts)- Added
LoadPagePartsOptionsinterface withpreRenderedPath?: string - Reads from pre-rendered file path if provided, otherwise from original
- Import resolution still uses original jay-html's directory
- Added
Dev server options (
dev-server/lib/dev-server-options.ts)- Added
buildFolder?: stringoption (defaults to<projectRootFolder>/build)
- Added
Dev server integration (
dev-server/lib/dev-server.ts)- Creates
SlowRenderCachewith cache dir at<buildFolder>/slow-render-cache/ - Three request handlers:
handleCachedRequest: Cache hit - skip slow render, use cached carryForward and file pathhandlePreRenderRequest: Cache miss - pre-render, write to disk, cache, then fast renderhandleLegacyRequest: Caching disabled - full viewState to client (backward compatible)
preRenderJayHtml()helper function for transformationsetupSlowRenderCacheInvalidation()for file watching
- Creates
Contract loading for phase detection
- Tries to load
.jay-contractfile beside the jay-html - File not found is OK (may use headless component contracts)
- Parse errors fail the function
- Tries to load
File watching for cache invalidation
- Watches
*.jay-htmlfiles - deletes cached file, invalidates entry - Watches
page.tsfiles - invalidates corresponding jay-html - Watches
*.jay-contractfiles - invalidates corresponding jay-html
- Watches
Files Created/Modified:
- NEW:
stack-server-runtime/lib/slow-render-cache.ts - MODIFIED:
stack-server-runtime/lib/load-page-parts.ts - MODIFIED:
stack-server-runtime/lib/index.ts - MODIFIED:
dev-server/lib/dev-server.ts - MODIFIED:
dev-server/lib/dev-server-options.ts
Tests: All existing dev-server tests pass (13 tests), all stack-server-runtime tests pass (66 tests)
Bug Fixes (Post Phase 5)
Fix 1: Nested Path Refs in slowForEach (2026-01-27)
Problem: When a slowForEach uses a nested path like categoryList.categories, the generated TypeScript had invalid property names:
export interface PageElementRefs {
categoryList.categories: { // Invalid - dots not allowed without quotes
Root Cause: In jay-html-compiler.ts, the nestRefs function was called with [arrayName] where arrayName = 'categoryList.categories' (the full dotted string as a single element). This created a RefsTree.children entry with a dotted key.
Fix: Changed nestRefs([arrayName], ...) to nestRefs(arrayName.split('.'), ...) so the path is properly split into segments like ['categoryList', 'categories'].
Files Modified:
compiler-jay-html/lib/jay-target/jay-html-compiler.ts(line ~820)
Fix 2: Negated Conditionals in Slow Render (2026-01-27)
Problem: Slow conditionals with negation like if="!imageUrl" were not being evaluated during slow rendering. The if attribute remained in the output instead of being resolved.
Root Cause: The parseBinding function only recognized simple property paths like imageUrl, but not negated expressions like !imageUrl (the ! prefix failed the regex).
Fix: Added analyzeSimpleCondition() function to expression-compiler.ts that returns both the property path and whether it's negated:
export interface AnalyzedCondition {
path: string; // The property path (without negation)
isNegated: boolean; // Whether the condition is negated
}
export function analyzeSimpleCondition(expr: string): AnalyzedCondition | null {
// Handles: "imageUrl", "product.name", "!imageUrl", "!product.isAvailable"
// Returns null for complex expressions (comparisons, logical operators)
}
The slow-render transform now imports and uses this shared function, ensuring:
- No code duplication between parsing locations
- Consistent behavior as condition expressions are extended
Files Created/Modified:
- MODIFIED:
compiler-jay-html/lib/expressions/expression-compiler.ts- AddedAnalyzedConditioninterface andanalyzeSimpleCondition()function - MODIFIED:
compiler-jay-html/lib/slow-render/slow-render-transform.ts- Import and useanalyzeSimpleCondition - NEW:
compiler-jay-html/test/fixtures/slow-render/conditional-negated-in-foreach/- Test fixture for negated conditionals inside forEach
Tests: All 437 compiler-jay-html tests passing (including new negated conditional test).
Fix 3: slowForEachItem Accessor Function for Nested Paths (2026-01-27)
Problem: For nested array paths like productSearch.filters.categoryFilter.categories, the runtime function slowForEachItem couldn't access the array because it only did:
const array = parentData[arrayName]; // Only works for 'categories', not 'a.b.c'
Root Cause: The compiler passed the array path as a string literal (e.g., 'productSearch.filters.categoryFilter.categories') and the runtime tried to use it as a single property key.
Fix: Changed slowForEachItem to accept an accessor function (like regular forEach):
// Before
slowForEachItem<ParentVS, ItemVS>(
arrayName: keyof ParentVS, // String property name
...
)
// After
slowForEachItem<ParentVS, ItemVS>(
getItems: (parentData: ParentVS) => ItemVS[], // Accessor function
...
)
The compiler now generates:
slowForEachItem<ViewState, Product>(
(vs: ViewState) => vs.productSearch.filters.categoryFilter.categories,
0, 'cat1', () => ...
)
Files Modified:
runtime/runtime/lib/element.ts- Changed function signaturecompiler-jay-html/lib/jay-target/jay-html-compiler.ts- Generate accessor function- Test fixtures updated
Fix 4: Headless Contract Loading for Text Bindings (2026-01-27)
Problem: Text bindings from headless components (e.g., {categoryName} from productSearch) were not being resolved during slow rendering, even though categoryName was phase: slow in the headless contract.
Root Cause: The phase map was only built from the page's main contract. Headless component contracts were not included, so their properties were treated as "unknown phase" and skipped during slow rendering.
Fix: Extended the slow-render system to include headless contracts:
- Extended
SlowRenderInputto acceptheadlessContracts?: HeadlessContractInfo[] - Updated
buildPhaseMapto include headless contract properties with their key prefix (e.g.,productSearch.categoryName) - Updated
LoadedPagePartsto include headless contracts fromparseJayFile - Dev server reuse: Instead of duplicating contract loading, the dev-server now gets headless contracts from
loadPageParts(which already callsparseJayFile→parseHeadlessImports)
Files Modified:
compiler-jay-html/lib/slow-render/slow-render-transform.ts- AddedHeadlessContractInfo, updatedbuildPhaseMapcompiler-jay-html/lib/index.ts- ExportHeadlessContractInfostack-server-runtime/lib/load-page-parts.ts- AddedheadlessContractstoLoadedPagePartsdev-server/lib/dev-server.ts- Pass headless contracts fromloadPagePartstopreRenderJayHtml
Key Learning: Avoid duplicating logic. The compiler's parseJayFile already loads headless contracts via parseHeadlessImports. Rather than re-implementing this in the dev-server, we extended LoadedPageParts to expose the already-loaded contracts.
Tests: Added 2 unit tests for headless contract support in slow-render-transform.test.ts.
Fix 5: Linked Sub-Contract Resolution for Slow Rendering (2026-01-28)
Problem: Properties from linked sub-contracts (e.g., products.thumbnail.url where products has link: ./product-card) were not being resolved during slow rendering. The thumbnail.url expression remained unresolved in the output even though it's a slow property.
Root Cause: The buildPhaseMap function only processed inline nested tags (tag.tags) but did not follow linked sub-contracts (tag.link). When a contract has a link property like link: ./product-card, the linked contract's properties were not added to the phase map.
Example:
# category-page.jay-contract
- tag: products
type: sub-contract
repeated: true
trackBy: _id
link: ./product-card # Linked contract - tags NOT processed!
The product-card.jay-contract has thumbnail.url as a slow property, but since the link wasn't followed, categoryPage.products.thumbnail.url wasn't in the phase map and wasn't resolved.
Fix: Extended the slow-render system to resolve linked contracts:
Extended
HeadlessContractInfoto includecontractPath?: string- the absolute path to the contract file for resolving relative linksExtended
JayHeadlessImportsto includecontractPath?: stringand populate it fromcontractFileinparseHeadlessImportsExtended
SlowRenderInputto acceptimportResolver?: JayImportResolverfor resolving linked contractsUpdated
buildPhaseMapto recursively resolve linked contracts:- Reuses existing
JayImportResolver.resolveLink()andloadContract()methods - Modified
processTag()to followtag.linkreferences and process the linked contract's tags - Properly resolves the linked contract's directory for nested links
- Reuses existing
Updated
loadPagePartsto includecontractPathwhen extracting headless contractsUpdated dev server to pass
JAY_IMPORT_RESOLVERwhen callingslowRenderTransform
Files Modified:
compiler-jay-html/lib/slow-render/slow-render-transform.ts- Added linked contract resolution using import resolvercompiler-jay-html/lib/jay-target/jay-html-source-file.ts- AddedcontractPathtoJayHeadlessImportscompiler-jay-html/lib/jay-target/jay-html-parser.ts- IncludecontractPathin resultstack-server-runtime/lib/load-page-parts.ts- IncludecontractPathin headless contractsdev-server/lib/dev-server.ts- PassJAY_IMPORT_RESOLVERtoslowRenderTransform
Key Design Decision: Reuse existing JayImportResolver interface for loading linked contracts rather than duplicating file I/O logic. This maintains a single source of truth for contract resolution.
Fix 6: Missing Slow-Phase Data Validation (2026-02-02)
Problem: When a slow-phase binding referenced a field that was undefined or null in the data, the binding was kept as-is (e.g., {title} remained in output). This caused:
- Broken-looking output with raw binding syntax visible to users
- Silent failures - no indication that data was missing
- Confusion between missing values and valid falsy values (0, '', false)
Solution:
- Add validation errors for missing slow-phase data - the build fails with descriptive error messages
- Render "undefined" as the output value - makes the issue visible in rendered HTML
- Distinguish truly missing values (undefined/null) from valid falsy values (0, '', false)
Behavior:
undefinedornull→ validation error + render "undefined"0,'',false→ no error, render as "0", "", "false" respectively
Example validation error:
Slow-phase binding {description} at path "description" has no value in slowViewState. Expected a value but got undefined.
Files Modified:
compiler-jay-html/lib/slow-render/slow-render-transform.ts- UpdatedresolveTextBindingsto validate missing datacompiler-jay-html/test/slow-render/slow-render-transform.test.ts- Added 6 tests for missing data handling
Tests Added:
should fail validation and render "undefined" for undefined slow-phase fieldshould fail validation and render "undefined" for null slow-phase fieldshould fail validation and render "undefined" for missing nested sub-contract fieldshould fail validation and render "undefined" for missing fields in forEach itemsshould fail validation and render "undefined" for missing image sub-contractshould correctly render valid falsy values (0, empty string, false)
Fix 7: Empty String Attributes Skipped During Slow Rendering (2026-02-10)
Problem: Slow-phase attribute bindings that resolved to an empty string "" were not being inlined. For example, <img src="{imageUrl}"> where imageUrl was "" in the data would leave {imageUrl} unresolved in the output. This caused a runtime error: "Cannot read properties of undefined (reading 'imageUrl')" because the client tried to resolve it as a fast-phase binding.
Root Cause: WithValidations.map() used if (this.val) to check for a value, which treated falsy values (empty string "", 0, false, null) as "no value". When resolveTextBindings returned "" for an empty imageUrl, the .map() callback that called element.setAttribute() was never executed.
Fix: Changed WithValidations.map(), flatMap(), mapAsync(), and flatMapAsync() to use if (this.val !== undefined) instead of if (this.val). This correctly distinguishes between undefined (no value) and valid falsy values like empty strings.
Files Modified:
compiler-shared/lib/with-validations.ts- Fixed 4 methods to use!== undefined- NEW:
compiler-shared/test/with-validations.test.ts- 12 tests for falsy value handling - NEW:
compiler-jay-html/test/fixtures/slow-render/attribute-empty-string/- Test fixture - NEW:
compiler-jay-html/test/fixtures/slow-render/foreach-empty-string-attr/- Test fixture for forEach + empty string compiler-jay-html/test/slow-render/slow-render-transform.test.ts- Added 2 tests
Tests: All 513 compiler-jay-html tests passing, 60 compiler-shared tests passing, 190 runtime tests passing.
Phase 6-7: Production Build & Incremental Regeneration (FUTURE)
Deferred to future implementation.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.