jit

package module
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 5, 2026 License: MIT Imports: 12 Imported by: 1

README

Fluent JIT

Just-In-Time optimisation strategies for the Fluent HTML5 component framework.

Features

Compile static content. Pre-render static portions of your templates once into a []byte, then execute a linear plan on subsequent renders. Dynamic content is re-evaluated at runtime using path-based navigation.

Adaptive buffer sizing. Learn optimal buffer sizes over repeated renders. Reduces memory allocations and garbage collection pressure without manual tuning.

Flatten for maximum speed. For fully static content, pre-render everything to a single []byte that's returned directly on every call. Strategy largely expects the use of .Static() calls. Dynamic nodes will cause an error when attempting to render.

Diff engine. Track keyed dynamic elements across renders and produce targeted patches for live updates. Powers Tether's reactive UI but works standalone. Supports full-tree diffs and single-key diffs via DiffKey.

Memoiser. An alternative to the Differ that skips unchanged subtrees entirely. Wraps content in node.Memoise with a cache key - when the key matches the previous render, the closure never runs. Use one or the other per session.

Documentation

  • Getting Started - add JIT to your Fluent app, step by step
  • Differ - keyed element tracking, targeted patches, snapshot persistence
  • Memoiser - skip unchanged subtrees with cache keys
  • AGENTS.md - comprehensive technical reference for LLMs

Install

go get github.com/jpl-au/fluent-jit

Requires Fluent as a dependency.

Optimisation Strategies

Fluent JIT provides three strategies, each with an Instance API and a Global API.

Compile

Works with any Fluent code. Analyses the node tree once, pre-renders static portions, and uses path-based navigation to re-evaluate dynamic nodes.

// Instance API - fine-grained control
compiler := jit.NewCompiler()
compiler.Render(myTemplate, w)  // First call builds plan + renders
compiler.Render(sameStructure, w)  // Reuses plan, re-evaluates dynamic content

// Global API - string-keyed registry
jit.Compile("homepage", myTemplate, w)

How it works:

  • Static subtrees become raw []byte chunks
  • Dynamic nodes (Text(), Textf(), RawText(), RawTextf(), Condition(), Func(), Funcs()) store paths for tree navigation
  • Adaptive buffer sizing optimises memory allocation over time

Important: The compiler expects the same tree structure on each call. Static content is frozen at first render; dynamic content is re-evaluated from the new tree.

Tune

Adaptive buffer sizing for any content. Learns optimal buffer sizes without compile-time analysis.

// Instance API
tuner := jit.NewTuner()
tuner.Tune(myTemplate).Render(w)

// Global API
jit.Tune("user-profile", myTemplate, w)

Use when:

  • Content patterns change over time
  • Attribute values vary between renders
  • You want buffer optimisation without compilation overhead
Flatten

Only works with fully static content. Pre-renders everything to a single []byte.

// Instance API - returns error if dynamic content found
flattener, err := jit.NewFlattener(staticTemplate)
if err != nil {
    // Contains dynamic content
}
flattener.Render(w)

// Global API - falls back to normal render if dynamic
jit.Flatten("footer", staticTemplate, w)

Use for: Headers, footers, navigation, any content that never changes.

Differ

Tracks keyed dynamic elements across renders and produces targeted patches for live updates. This is the engine behind Tether's reactive UI, but can be used standalone.

differ := jit.NewDiffer()

// Initial render - stores snapshots of all keyed elements
html := differ.Render(tree)

// After state change - returns only what changed
patches, change := differ.Diff(newTree)

if change != nil {
    // Structural change - keys were added, removed, or reordered.
    // change.String() describes what happened, e.g. "key 'sidebar' added"
    html = differ.Render(newTree)
} else {
    for _, p := range patches {
        // p.Key identifies which element, p.HTML is the new content
    }
}

Mark elements for tracking with .Dynamic("key"). The Differ only tracks outermost keyed elements - if a parent and child are both keyed, only the parent is tracked.

div.New(
    span.Textf("Count: %d", count).Dynamic("count"),  // Tracked
    span.Static("Footer"),                              // Ignored
)

Use Validate to catch duplicate keys at startup:

if err := differ.Validate(tree); err != nil {
    log.Fatal(err)  // "duplicate dynamic key in render tree: "count""
}
Snapshot persistence

The Differ supports exporting and importing its snapshot state as opaque bytes, useful for offloading disconnected session data (e.g. via Tether's DiffStore interface).

// Export returns the snapshot data as raw bytes (nil if not seeded)
data := differ.Export()

// Import restores snapshots from a prior Export
if err := differ.Import(data); err != nil {
    log.Fatal(err)
}

// Clear releases snapshot buffers back to the pool
differ.Clear()

The encoding is opaque - callers must not interpret or manipulate the bytes. Export is non-destructive and does not clear the Differ's state.

DiffKey (targeted single-key diff)

When you know exactly which key changed, DiffKey re-renders and diffs only that key against the stored snapshot. The rest of the tree is untouched. Over 1,000x faster than a full Diff for targeting one key out of many.

patch := differ.DiffKey("count", span.Textf("Count: %d", newCount).Dynamic("count"))
if patch != nil {
    // patch.Key is "count", patch.HTML is the new content
}

DiffKey updates the snapshot for the targeted key, so subsequent Diff calls see the new content. Other keys are unaffected.

Memoiser

An alternative to the Differ that skips unchanged subtrees. Each Dynamic region wraps its content in node.Memoise with a cache key. When the key matches the previous render, the closure never runs and no HTML is produced for that region.

memoiser := jit.NewMemoiser()

// Initial render - stores snapshots and memoisation keys
html := memoiser.Render(tree)

// After state change - skips unchanged subtrees
patches, change := memoiser.Diff(newTree)

The Memoiser is a standalone engine, not a wrapper around the Differ. Use one or the other per session, not both. Both support DiffKey for targeted single-key diffs.

The render function uses node.Memoise to mark skippable regions:

div.New(
    node.Memoise(version, func() node.Node {
        return expensiveRender()
    }),
).Dynamic("items")

When version matches the previous render, the closure is not called and the stored snapshot is reused. When it changes, the closure runs and the result is diffed against the previous snapshot.

Configuration

Both Compiler and Tuner support custom configuration:

// Compiler configuration
compiler := jit.NewCompiler(&jit.CompilerCfg{
    Threshold:    15,  // Deviation % before updating buffer stats
    Max:          5,   // Samples before establishing baseline
    Variance:     20,  // Threshold % for detecting size changes
    GrowthFactor: 115, // Multiplier % for average size
})

// Or configure after creation
compiler.Configure(threshold, max, variance, growthFactor)

// Tuner configuration
tuner := jit.NewTuner(&jit.TunerCfg{
    Max:          5,
    Variance:     20,
    GrowthFactor: 115,
})

Global API Memory Warning

The global API uses sync.Map registries that grow indefinitely. Use constant string IDs (e.g., "header", "footer"). If using dynamic IDs, manually reset when no longer needed.

// Clear specific entries
jit.ResetCompile("homepage")
jit.ResetTune("user-profile")
jit.ResetFlatten("footer")

// Clear all entries
jit.ResetCompile()
jit.ResetTune()
jit.ResetFlatten()

When to Use JIT

The base Fluent API already performs well with automatic buffer pooling. JIT optimisation is for squeezing out extra performance in high-throughput scenarios.

Recommendations:

  1. Build and test without JIT first
  2. Profile to identify actual bottlenecks
  3. Apply JIT selectively where it matters

Profile-Guided Optimization (PGO)

Applications using Fluent JIT benefit from Profile-Guided Optimization (Go 1.21+). PGO uses a CPU profile from your running application to make more aggressive inlining decisions at compile time, improving the JIT compilation, tuning, and flattening paths. Expect 10-20% speed improvements with no code changes.

  1. Collect a CPU profile under realistic load:
    curl -o default.pgo http://localhost:8080/debug/pprof/profile?seconds=30
    
  2. Place default.pgo in your main package directory
  3. go build - PGO is applied automatically

Allocations are unaffected; PGO improves speed only. Collect fresh profiles periodically as your application evolves.

Licence

MIT

Documentation

Overview

Package jit provides Just-In-Time optimisation strategies for Fluent HTML rendering. Each strategy targets a different performance profile. The base Fluent API already performs well with automatic buffer pooling; JIT is for squeezing out extra performance in high-throughput scenarios.

Choosing a Strategy

Start by asking what your content looks like and what you need back.

Is the content fully static (no Text, Func, Condition)?

Yes -> Flatten. Pre-renders once to []byte. Subsequent calls are
       a memory copy. Use for headers, footers, navigation, any
       content built entirely from Static() calls.

Does the content mix static and dynamic parts?

Yes, and I just need fast renders -> Compile. Freezes static
       portions on first render, re-evaluates only dynamic nodes
       via path-based navigation. Best general-purpose strategy.

Yes, but the tree structure changes between renders -> Tune.
       Adaptive buffer sizing without structural assumptions.
       No compilation step, no frozen plan. Works with any content
       that changes shape over time.

Do I need to know what changed between renders (patches)?

Yes, and rebuilding the tree is cheap -> Differ. You rebuild the
       full tree on each state change. The Differ walks both trees,
       compares keyed elements, and returns Patch values for only
       the elements whose HTML actually changed.

Yes, but some subtrees are expensive to rebuild -> Memoiser.
       Wraps expensive subtrees in node.Memoise with a cache key.
       When the key matches the previous render, the closure never
       runs. Only changed subtrees produce new HTML for diffing.

Differ vs Memoiser

These two share the same interface (Render, Diff, DiffKey, Export, Import) and produce the same output ([]Patch, *StructuralChange). They differ in how they decide whether content changed.

The Differ is content-based. It renders every keyed element on every Diff call and compares the HTML bytes against stored snapshots. If the bytes match, no patch is emitted. This is simple and correct but means every closure runs on every diff, even if nothing changed.

differ := jit.NewDiffer()
html := differ.Render(buildTree(state))     // initial render
patches, change := differ.Diff(buildTree(newState))  // full re-render + compare

The Memoiser is key-based. Each Dynamic region wraps its content in node.Memoise with a cache key (typically a version counter, hash, or timestamp). When the key matches, the closure is skipped entirely - no HTML is produced, no comparison is needed. When the key differs, the closure runs and the result is compared against the snapshot.

memoiser := jit.NewMemoiser()
html := memoiser.Render(buildTree(state))   // initial render
patches, change := memoiser.Diff(buildTree(newState)) // skips unchanged closures

Use Differ when:

  • Rebuilding the tree is cheap (simple templates, small data)
  • You don't have natural cache keys for your data
  • You want the simplest mental model (rebuild everything, diff finds changes)

Use Memoiser when:

  • Some subtrees are expensive (database queries, complex formatting)
  • You have natural version identifiers (timestamps, counters, hashes)
  • You want to avoid running closures for unchanged regions
  • You have many keyed elements but few change on each update

Both support DiffKey for targeted single-key diffs when you know exactly which element changed. DiffKey is over 1,000x faster than a full Diff for targeting one key out of many.

Use one or the other per session, not both. They maintain independent snapshot state and mixing them produces incorrect diffs.

Keying Nodes for Tracking

The diff engine tracks any node that satisfies node.Dynamic and returns a non-empty key from DynamicKey(). Elements use .Dynamic("key") to set this. Function and conditional nodes support the same method:

// Element - tracked as "counter"
span.Textf("Count: %d", n).Dynamic("counter")

// Function - tracked as "greeting"
node.Func(func() node.Node {
    return div.Text(greetUser())
}).Dynamic("greeting")

// Conditional - tracked as "auth"
node.When(loggedIn, welcomePanel).Dynamic("auth")

// Multi-node function - tracked as "items"
node.Funcs(func() []node.Node {
    return buildItems(data)
}).Dynamic("items")

Without a key, the differ cannot produce targeted patches for that node. Keys must be unique within a render tree.

Combining Strategies

Strategies operate at different levels and can be combined:

  • Compile + Differ: Compile handles the render path (freezing static content), Differ handles the diff path (tracking changes). Use Compile for the initial full render, Differ for subsequent updates.

  • Flatten for static regions, Compile for dynamic templates: Different parts of the page can use different strategies. A static nav bar uses Flatten; the main content area uses Compile.

Instance API vs Global API

Each strategy has two ways to use it:

  • Instance API: Create with NewCompiler, NewTuner, NewFlattener, NewDiffer, or NewMemoiser. You own the lifecycle. Use when you need per-session or per-component control.

  • Global API: Package-level Compile, Tune, and Flatten functions with string IDs. Managed in a global registry. Convenient for application-wide templates with fixed IDs. The registry grows indefinitely - use constant IDs, not user-derived strings.

The Differ and Memoiser are instance-only. They track per-session state (snapshots, keys, ordering) that doesn't suit a global registry.

Index

Constants

This section is empty.

Variables

View Source
var ErrDuplicateKey = fmt.Errorf("duplicate dynamic key in render tree")

ErrDuplicateKey is returned when a render tree contains duplicate dynamic keys. Keys must be unique within a tree so the diff engine can unambiguously track each dynamic element across renders.

View Source
var ErrDynamicContent = errors.New("NewFlattener() requires static content - use NewCompiler() for dynamic content")

ErrDynamicContent is returned when attempting to flatten dynamic content. The flattener can only cache static content - dynamic nodes must be re-evaluated on each render, which defeats the purpose of flattening.

View Source
var ErrStructureMismatch = errors.New("node tree structure does not match the compiled execution plan")

ErrStructureMismatch indicates that a node tree passed to Compiler.Validate has a different structure than the tree used to build the execution plan. The compiler navigates dynamic nodes by their position in the tree (stored as index paths), so a structural change means those paths no longer resolve to the correct nodes - producing truncated or incorrect output.

View Source
var SnapshotHint = 128

SnapshotHint is the initial capacity hint in bytes for snapshot buffers. Most keyed elements render to small HTML fragments, so 128 bytes avoids an early grow in the common case. Adjust if your elements are typically larger or smaller.

Functions

func Compile

func Compile(id string, n node.Node, w ...io.Writer) []byte

Compile looks up a compiler by ID in a global registry, creating it if it doesn't exist, and renders it using the compilation strategy. If CompileConfig() was called first, that config will be used.

The node is used both to build the plan (on first call) and to provide dynamic content for rendering. Static content is frozen from the first call.

Warning: The global registry grows indefinitely. Do not use dynamic IDs without manually calling ResetCompile(id) to free memory.

func CompileConfig

func CompileConfig(id string, cfg CompilerCfg)

CompileConfig creates a compiler instance with custom configuration. Must be called before first Compile() call for the given ID.

func Flatten

func Flatten(id string, n node.Node, w ...io.Writer) []byte

Flatten looks up flattened static content in the global registry. On first call with a node, it validates the content is static, renders it once, and stores the result. Subsequent calls retrieve the stored bytes.

Unlike NewFlattener which returns an error for dynamic content, this silently falls back to uncached rendering. This avoids disrupting request handlers where returning an error would be impractical.

Warning: The global registry grows indefinitely. Do not use dynamic IDs without manually calling ResetFlatten(id) to free memory.

func ResetCompile

func ResetCompile(ids ...string)

ResetCompile removes compiled templates from the global registry, allowing them to be re-compiled on next use. Call with no arguments to clear all entries, or pass specific IDs to remove.

func ResetFlatten

func ResetFlatten(ids ...string)

ResetFlatten removes flattened static content from the global registry. Call with no arguments to clear all entries, or pass specific IDs to remove.

func ResetTune

func ResetTune(ids ...string)

ResetTune removes tuned templates from the global registry, causing their tuning statistics to be reset on next use. Call with no arguments to clear all entries, or pass specific IDs to remove.

func Tune

func Tune(id string, n node.Node, w ...io.Writer) []byte

Tune looks up a tuner by ID in a global registry, creating it if it doesn't exist, and renders it using the adaptive tuning strategy. If TuneConfig() was called first, that config will be used.

Warning: The global registry grows indefinitely. Do not use dynamic IDs without manually calling ResetTune(id) to free memory.

func TuneConfig

func TuneConfig(id string, cfg TunerCfg)

TuneConfig creates a tuner instance with custom configuration. Must be called before first Tune() call for the given ID.

Types

type AdaptiveSizer

type AdaptiveSizer struct {
	// contains filtered or unexported fields
}

AdaptiveSizer implements adaptive buffer sizing with minimal lock contention. It operates in two phases:

1. Sampling Phase: Collects render size samples to establish optimal buffer size. 2. Baseline Phase: Uses established size with variance monitoring for pattern changes.

Performance characteristics: - Hot path (GetBaseline): lock-free atomic read - called on every render. - Warm path (variance checks): occasional mutex for pattern change detection. - Cold path (sampling): mutex for statistical calculations during startup.

func NewAdaptiveSizer

func NewAdaptiveSizer() *AdaptiveSizer

NewAdaptiveSizer creates a sizer with sensible defaults. Default configuration: - max: 5 samples (quick baseline establishment). - variance: 20% (detects significant size changes). - growthFactor: 115% (prevents buffer resizing on small variations). - active: true (starts in sampling phase).

func (*AdaptiveSizer) Active

func (as *AdaptiveSizer) Active() bool

Active returns true if currently in sampling phase. Lock-free read for performance.

func (*AdaptiveSizer) Configure

func (as *AdaptiveSizer) Configure(max int, variance, growthFactor int)

Configure sets custom parameters and resets all statistics. This forces the sizer to restart sampling with new parameters, because stale statistics from previous configuration would produce an incorrect baseline.

Parameters: - max: number of samples to collect before establishing baseline. - variance: threshold percentage for detecting significant size changes (e.g. 20). - growthFactor: multiplier percentage applied to average size (e.g. 115).

func (*AdaptiveSizer) GetBaseline

func (as *AdaptiveSizer) GetBaseline() int

GetBaseline returns the current optimal buffer size. This is the hot path - called on every render - so it uses a lock-free atomic read to avoid contention.

func (*AdaptiveSizer) Reset

func (as *AdaptiveSizer) Reset()

Reset clears all statistics and restarts sampling. Useful when content patterns change significantly.

func (*AdaptiveSizer) UpdateStats

func (as *AdaptiveSizer) UpdateStats(size int)

UpdateStats updates sizing statistics based on actual render size. This automatically chooses between sampling and variance checking based on the current phase.

type CompiledElement

type CompiledElement interface {
	Render(originalTree node.Node, buf *bytes.Buffer)
}

CompiledElement represents a single rendering operation in the execution plan. Elements are either pre-rendered static content or dynamic node references.

type Compiler

type Compiler struct {
	// contains filtered or unexported fields
}

Compiler builds immutable execution plans with optimised buffer sizing. It separates static and dynamic content during compilation, then uses conditional statistical updates to maintain optimal buffer allocation.

func NewCompiler

func NewCompiler(cfg ...*CompilerCfg) *Compiler

NewCompiler creates a compiler with sensible defaults. Default threshold: 15% deviation before updating buffer size statistics.

func (*Compiler) Configure

func (jc *Compiler) Configure(threshold int, max int, variance, growthFactor int) *Compiler

Configure customises the compiler's threshold and adaptive sizing parameters. Returns the same instance for method chaining.

func (*Compiler) Render

func (jc *Compiler) Render(root node.Node, w ...io.Writer) []byte

Render builds the execution plan on first call, then renders the node. Subsequent calls reuse the existing plan with fresh dynamic content from the provided tree.

Static content (including attributes) is frozen from the first call. Dynamic content is re-evaluated from the provided tree on each call.

Example:

compiler := jit.NewCompiler()
compiler.Render(UserCard("Alice", 30), w)  // builds plan + renders Alice
compiler.Render(UserCard("Bob", 25), w)    // reuses plan, renders Bob
compiler.Render(UserCard("Dan", 40), w)    // reuses plan, renders Dan

func (*Compiler) Validate added in v0.1.2

func (jc *Compiler) Validate(root node.Node) error

Validate checks whether a node tree is structurally compatible with the compiled execution plan. It walks each DynamicPath in the plan and verifies that the path resolves to a valid node in the provided tree.

This is a diagnostic tool for tests and development - it should NOT be called in production because it adds overhead to every render. In production, a structure mismatch will produce visibly broken output, which is sufficient signal to investigate.

Returns nil if the tree is compatible, or ErrStructureMismatch with details about which path failed.

Example (in a test):

compiler := jit.NewCompiler()
compiler.Render(baseTree)          // builds plan
if err := compiler.Validate(newTree); err != nil {
    t.Fatalf("tree structure changed: %v", err)
}

type CompilerCfg

type CompilerCfg struct {
	Threshold    int // deviation threshold percentage for conditional stats updates
	Max          int // samples before establishing baseline
	Variance     int // threshold percentage for detecting size changes
	GrowthFactor int // multiplier percentage for average size
}

CompilerCfg holds configuration for JIT compiler instances.

type Differ added in v0.2.0

type Differ struct {
	// contains filtered or unexported fields
}

Differ tracks rendered output of keyed dynamic nodes across renders and produces targeted patches when their content changes.

Each session should own its own Differ - they are not shared across sessions. The typical lifecycle is:

  1. Render() on initial page load - returns full HTML, stores snapshots
  2. Diff() after each state change - returns patches for changed elements
  3. If Diff returns a *StructuralChange, call Render() again for a full re-render

Snapshot data can be serialised for external storage via Differ.Export, restored with Differ.Import, and freed with Differ.Clear. This supports offloading disconnected session data to reduce memory usage.

func NewDiffer added in v0.2.0

func NewDiffer() *Differ

NewDiffer creates a new Differ instance.

func (*Differ) Clear added in v0.2.1

func (d *Differ) Clear()

Clear releases the differ's snapshot buffers back to the pool and resets it to an unseeded state. Call this after a successful DiffStore.Save to free the memory that Export copied out.

func (*Differ) Diff added in v0.2.0

func (d *Differ) Diff(root node.Node) ([]Patch, *StructuralChange)

Diff compares the new tree against stored snapshots and returns targeted patches for any keyed dynamic nodes whose content changed.

Returns (patches, nil) when all keys match between renders. The patches slice is nil if nothing changed.

Returns (nil, *StructuralChange) when keys were added, removed, or reordered - the caller should use Render for a full re-render and can use the StructuralChange for diagnostics.

Returns (nil, nil) if Render has not been called yet.

func (*Differ) DiffKey added in v0.2.3

func (d *Differ) DiffKey(key string, subtree node.Node) *Patch

DiffKey re-renders a single Dynamic key against the stored snapshot and returns a patch if the content changed. Use this for targeted updates where the caller knows exactly which key changed and wants to avoid the cost of a full tree walk via Differ.Diff. For a page with 50 Dynamic keys, DiffKey is over 1,000x faster than Diff.

The snapshot for the targeted key is updated so subsequent Diff calls see the new content. Other keys are not touched.

Returns nil if the content is unchanged. Returns a patch with the new HTML if the content changed or the key has no stored snapshot.

func (*Differ) Export added in v0.2.1

func (d *Differ) Export() []byte

Export returns the differ's snapshot data as raw bytes suitable for external storage. The differ's internal state is unchanged - call Clear to release the memory after a successful save. Returns nil if the differ has not been seeded.

The encoding is an internal detail. Callers must not interpret the bytes - use Import to restore them.

func (*Differ) Import added in v0.2.1

func (d *Differ) Import(data []byte) error

Import restores snapshot data from a prior Export call. The differ is marked as seeded after a successful import, allowing Diff to compare against the restored snapshots.

func (*Differ) Render added in v0.2.0

func (d *Differ) Render(root node.Node, w ...io.Writer) []byte

Render produces the full HTML for the tree and stores snapshots of all keyed dynamic nodes. Use this for the initial page load and after structural changes detected by Diff.

If a writer is provided, the HTML is written to it and nil is returned. If no writer is provided, the HTML is returned as a byte slice.

func (*Differ) Validate added in v0.2.0

func (d *Differ) Validate(root node.Node) error

Validate checks a tree for duplicate dynamic keys. Keys must be unique within a tree so the diff engine can track each element unambiguously. Returns nil if all keys are unique.

type DynamicPath

type DynamicPath struct {
	Path []int // Indices to navigate: e.g., [0, 1] means root.Nodes()[0].Nodes()[1]
}

DynamicPath holds the path to a dynamic node in the tree structure. The path is a slice of indices that navigates from root to the dynamic node. This enables re-evaluation with new tree instances that share the same structure.

func (*DynamicPath) Render

func (dp *DynamicPath) Render(root node.Node, buf *bytes.Buffer)

Render navigates the tree using the stored path and renders the dynamic node. This allows different tree instances (with same structure) to render different values.

type ExecutionPlan

type ExecutionPlan struct {
	Elements []CompiledElement // Linear sequence of rendering operations
}

ExecutionPlan contains the compiled sequence of static and dynamic elements. The plan is a linear sequence that can be executed without tree traversal.

type Flattener

type Flattener struct {
	// contains filtered or unexported fields
}

Flattener holds pre-rendered static content as bytes. This is the instance API for static content rendering - no map lookups, just direct byte access. Ideal for maximum performance with static templates.

func NewFlattener

func NewFlattener(n node.Node) (*Flattener, error)

NewFlattener creates a flattener by rendering static content once. Returns an error if the node contains dynamic content.

func (*Flattener) Render

func (f *Flattener) Render(w ...io.Writer) []byte

Render writes the pre-rendered bytes to the writer or returns them. No rendering logic is executed - this is a direct byte slice write.

type Memoiser added in v0.2.3

type Memoiser struct {
	// contains filtered or unexported fields
}

Memoiser provides an alternative to Differ for render trees that use node.Memoise nodes. It is a standalone concern - use either the Differ or the Memoiser, not both on the same session.

When the developer opts into memoisation, every Dynamic region in the render tree should contain a node.Memoise child with a cache key. On each Diff call, the Memoiser compares memoisation keys with the previous render. Matching keys skip the subtree entirely - the closure never runs and no HTML is rendered. Mismatched keys call the closure and render the result into a snapshot for comparison.

The Memoiser does not fall back to content-based diffing for non-memoised Dynamic nodes. If a Dynamic node has no memoised child, it is always re-rendered (treated as a miss). This keeps the implementation simple and fast - there is no tree walking via Nodes() that would materialise closures.

func NewMemoiser added in v0.2.3

func NewMemoiser() *Memoiser

NewMemoiser creates an empty Memoiser ready for use.

func (*Memoiser) Clear added in v0.2.3

func (m *Memoiser) Clear()

Clear releases snapshot buffers and resets all state.

func (*Memoiser) Diff added in v0.2.3

func (m *Memoiser) Diff(root node.Node) ([]Patch, *StructuralChange)

Diff compares the new tree against stored snapshots using memoisation keys. For each Dynamic node:

  • If its child satisfies node.Memoiser and the key matches the previous render, the subtree is skipped. No closure is called, no HTML is rendered. The previous snapshot is reused.
  • If the key differs (or there is no memoised child), node.Memoiser.MemoiseRender is called (or the node is rendered directly) and the result is compared against the stored snapshot.

Returns (patches, nil) when Dynamic keys match between renders. Returns (nil, *StructuralChange) when keys were added, removed, or reordered. Returns (nil, nil) if Render has not been called.

func (*Memoiser) DiffKey added in v0.2.3

func (m *Memoiser) DiffKey(key string, subtree node.Node) *Patch

DiffKey re-renders a single Dynamic key against the stored snapshot and returns a patch if the content changed. Use this for targeted updates where the caller knows exactly which key changed. Does not check memoisation keys because the developer is explicitly targeting this key. The snapshot is updated so subsequent Diff calls see the new content.

func (*Memoiser) Export added in v0.2.3

func (m *Memoiser) Export() []byte

Export returns the Memoiser's snapshot and memoisation key data as raw bytes suitable for external storage. Returns nil if unseeded.

func (*Memoiser) Import added in v0.2.3

func (m *Memoiser) Import(data []byte) error

Import restores snapshot and memoisation key data from a prior Export.

func (*Memoiser) Render added in v0.2.3

func (m *Memoiser) Render(root node.Node, w ...io.Writer) []byte

Render produces the full HTML for the tree and stores snapshots and memoisation keys for all Dynamic regions. Use this for the initial page load and after structural changes detected by Diff.

func (*Memoiser) Stats added in v0.2.3

func (m *Memoiser) Stats() (hits, misses int)

Stats returns the hit and miss counts from the most recent Diff call. A hit means the memoisation key matched and the subtree was skipped. A miss means the key differed (or was absent) and the subtree was re-rendered. Call this immediately after Diff to inspect cache performance.

func (*Memoiser) Validate added in v0.2.3

func (m *Memoiser) Validate(root node.Node) error

Validate checks a tree for duplicate dynamic keys.

type Patch added in v0.2.0

type Patch struct {
	Key  string
	HTML []byte
}

Patch represents a targeted change to a dynamic element in the rendered output. Key matches the value passed to .Dynamic("key") on the element. HTML is the new rendered content for that element.

type StaticContent

type StaticContent struct {
	Content []byte // Pre-rendered HTML bytes ready for direct buffer writes
}

StaticContent holds pre-rendered static HTML content as raw bytes. Adjacent static nodes are merged into single StaticContent elements for efficiency.

func (*StaticContent) Render

func (sc *StaticContent) Render(_ node.Node, buf *bytes.Buffer)

Render writes the pre-compiled static content directly to the buffer. This is extremely fast as it's just a memory copy operation.

type StructuralChange added in v0.2.0

type StructuralChange struct {
	Added     []string // keys present in the new tree but not the old
	Removed   []string // keys present in the old tree but not the new
	Reordered bool     // same keys, different order
}

StructuralChange describes why a diff detected a structural change. The caller can use this to produce actionable diagnostics that tell the developer exactly what changed and how to avoid root morphs.

func (*StructuralChange) String added in v0.2.0

func (c *StructuralChange) String() string

String returns a human-readable description of the change, e.g. "key 'sidebar' added" or "keys reordered".

type Tuner

type Tuner struct {
	// contains filtered or unexported fields
}

Tuner provides dynamic adaptive buffer sizing for changing content patterns. Unlike the compiler which pre-optimises static content, the tuner adapts to content that changes over time by continuously monitoring render sizes.

The tuner uses shared AdaptiveSizer logic with two-phase operation: 1. Sampling phase: Collects render size samples to establish optimal buffer size 2. Baseline phase: Uses established size with variance monitoring for pattern changes

This approach is ideal for templates with dynamic content that varies significantly.

func NewTuner

func NewTuner(cfg ...*TunerCfg) *Tuner

NewTuner creates a tuner with adaptive sizing defaults. Uses shared AdaptiveSizer with standard configuration: - 5 samples for baseline establishment. - 20% variance threshold for pattern change detection. - 115% growth factor to prevent tight buffer fits.

func (*Tuner) Configure

func (jt *Tuner) Configure(max int, variance, growthFactor int) *Tuner

Configure customises the adaptive sizing parameters and resets statistics. This forces the tuner to restart sampling with new parameters.

Parameters: - max: number of samples to collect before establishing baseline. - variance: threshold percentage for detecting significant size changes (e.g. 20). - growthFactor: multiplier percentage applied to average size (e.g. 115).

func (*Tuner) Render

func (jt *Tuner) Render(w ...io.Writer) []byte

Render executes the configured template with adaptive buffer sizing. This method automatically optimises buffer allocation based on historical render sizes and continuously updates statistics for future optimisation.

func (*Tuner) Reset

func (jt *Tuner) Reset() *Tuner

Reset clears all collected statistics and restarts adaptive sizing. Useful when content patterns change significantly or for testing scenarios. Returns the same instance for method chaining.

func (*Tuner) Tune

func (jt *Tuner) Tune(root node.Node) *Tuner

Tune sets the template to render with adaptive buffer sizing. Thread-safe for concurrent usage. Returns the same instance for method chaining.

type TunerCfg

type TunerCfg struct {
	Max          int // samples before establishing baseline
	Variance     int // threshold percentage for detecting size changes
	GrowthFactor int // multiplier percentage for average size
}

TunerCfg holds configuration for JIT tuner instances.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL