This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Quick wins: small, targeted code changes (low risk)
-
Reuse arrays in
BindingContext.prepareParametersForEvaluation(impact: medium)- File:
src/template/binding-context.ts - Today it re-allocates
preBuiltParameters,preBuiltParameterKeys, andpreBuiltParameterValuesevery render and even duplicates work when a parent exists (lines ~87–105). Reuse the same arrays and mutate with push/pop, or keep a simple linked structure to avoid copying parent arrays on every render. This reduces GC churn on frequent updates.
- File:
-
Memoize
preBuiltParameterKeys.join(",")for eval cache (impact: low–medium)- File:
src/template/binding-context.ts eval()buildscacheKey = this.preBuiltParameterKeys.join(",") + js;on every evaluation (lines ~115–119). For many bindings this is a hot path. Persist the joined key when the parameter set changes, so you avoid repeated joins and string allocations.
- File:
-
Avoid
with(this)if possible, or at least limit its scope (impact: medium)- File:
src/template/binding-context.ts - The created function uses
with(this)(line ~133).withimpairs JS engine optimizations. If feasible, generate functions that explicitly reference the required identifiers via parameters and avoidwith. If keepingwith, restrict usage to less frequent code paths.
- File:
-
Deduplicate queued renders before executing (impact: high for burst updates)
- File:
src/template/binding-proxy-handler.ts setenqueues a task per binding without dedupe (lines ~68–84). If the same binding/key is set multiple times in a microtask, you render multiple times. Maintain aMap<Binding, Set<string>>(or combined key) inQueuedTasksso each binding/key pair renders once per microtask. Call once per binding with a single coalescedRenderingContextor loop unique keys.
- File:
-
Reinstate and improve microtask optimization for array ops (impact: medium–high for large lists)
- File:
src/template/binding-proxy-handler.ts optimize()pusheslengthupdates to the end but is commented out at call-site (line ~79). Re-enable and expand: coalesce 'length' with per-index updates, prefer a single pass list diff.
- File:
-
Use
WeakMapfor per-target bindings to prevent leaks (impact: medium)- File:
src/template/binding-proxy-handler.ts bindingsSymbolmap is stored on the target object. For objects not under your control this can retain references. Consider aWeakMap<object, Map<PropertyKey, Binding[]>>owned by the handler, or ensure cleanup when a binding is destroyed.
- File:
-
Fast path skip for attribute-less nodes in
compile(impact: low–medium)- File:
src/template/template-compilation.ts - In
compileNode(), each descriptor’scompileinspects attributes for every node (lines ~60–70). Add quick guards: if!(node instanceof HTMLElement)skip attribute-based compilers; ifnode instanceof Text && !includes("${")skip text binding compile call. This reduces redundant checks.
- File:
-
Tighten loops and cache lengths (impact: low)
- Multiple files (e.g.,
template-compilation.ts,template-renderer.ts,event-binding-descriptor.ts,property-binding-descriptor.ts) - Replace
for (let i = 0; i < arr.length; i++)with cached lengthfor (let i = 0, n = arr.length; i < n; i++)in hot paths. Minor but measurable in tight render loops.
- Multiple files (e.g.,
-
Optimize property lookup during compilation (impact: medium for larger templates)
- File:
src/template/bindings/property-binding-descriptor.ts - The code scans
for (let key in node)to match property name case-insensitively (lines ~25–29). This enumerates many props. Prefer checking direct presenceif (targetPropName in node)and use a small lowercase-to-actual-name cache per tagName to avoid repeated enumeration.
- File:
-
Avoid clearing entire parent for empty repeaters (impact: medium)
- File:
src/template/bindings/repeater-binding-descriptor.ts - When
newContexts.length <= 0it callsparent.replaceChildren()then re-appends the anchor (lines 72–77). This nukes all siblings. Safer and faster: delete the range that covers only repeated nodes and leave the anchor untouched (create range fromcommentNode.nextSiblingto last rendered node; delete if present).
- File:
-
Batch DOM insertion using a single
DocumentFragment(impact: medium)- File:
src/template/bindings/repeater-binding-descriptor.ts - Currently creates
newNodes = template.newTemplateElements(...)and thencommentNode.before(...newNodes)(line 69). Better: append those nodes to a singleDocumentFragmentand insert once; while spread insertion is already batched, fragment insert can be slightly faster and avoids building an intermediate array if you generate directly into the fragment.
- File:
-
Minimize
Rangecreation in repeater shrink (impact: low–medium)- File:
src/template/bindings/repeater-binding-descriptor.ts - Each shrink creates a new
Range(lines 79–83). Reuse a singleRangeinstance stored inrenderDataor use node removal vianextSiblingloop for fewer allocations.
- File:
-
Early return in
renderDescriptorfor repeaters when nothing changed (impact: medium)- File:
src/template/bindings/repeater-binding-descriptor.ts - If
newContexts.length === renderData.renderers.lengthandrenderingContextdoesn’t target the repeater’smodelName, skip. You already short-circuit onrenderingContext.key === modelNamefor updates (lines 54–59), but you still run creation/deletion checks. Introduce a quick equality check to exit early.
- File:
-
Avoid cloning an entire
HTMLElementjust to extract outerHTML (impact: low–medium)- File:
src/template/bindings/repeater-binding-descriptor.ts tplNode = node.cloneNode(true) as HTMLElement; tplNode.attributes.removeNamedItem("repeat.for"); ... tplNode.outerHTML(lines ~28–33). You can read the originalouterHTMLand removerepeat.forvia string replacement at compile time, or better, clone minimally and setinnerHTMLstring. Reduces DOM work during compile.
- File:
-
Use
Text.nodeValueassignment sparingly (impact: low)- File:
src/template/bindings/text-binding-descriptor.ts - Already guarded by last value (22–26). Additionally, call
textNode.nodeValue = valvs.datais equivalent; keep as is, but ensurevalis string to avoid implicit coercion costs in hot paths.
- File:
-
Avoid creating closures per event dispatch (impact: low)
- File:
src/template/bindings/event-binding-descriptor.ts - Current code creates arrow function each time you connect (lines 38–40). Store the listener function and reuse; you already store
lastEventListener, but the wrapper arrow still allocates. Keep a bound function and use it directly.
- File:
-
Use
DocumentFragmentfor root binding append (impact: low)- File:
src/template/template-renderer.ts appendFunction(this.bindings.element)is fine. Ensure upstreamappendFunctiontypically appends a fragment or uses batching to avoid layout thrash.
- File:
Bigger proposals: architectural or deep refactors
-
Keyed diffing for repeaters with move detection (impact: high)
- File:
src/template/bindings/repeater-binding-descriptor.ts - Currently the repeater only grows/shrinks and re-renders all existing items when
renderingContext.key === modelName. It neither reorders nor reuses children by identity. Introduce keyed diffing by the array keys (BindingContext.scopedModelKey) or a supplied key function. Compute LCS or a simple index map to support moves with minimal DOM ops: insert/move/remove only what changed and only re-render affected items.
- File:
-
Stable item renderer instances with keyed caching (impact: high)
- File:
src/template/bindings/repeater-binding-descriptor.ts - Maintain a
Map<key, Binding>for item renderers. On update, reuse renderer instances by key, update contexts, and move DOM nodes instead of destroying/recreating or re-rendering in place. This avoids unnecessary re-binding and preserves stateful child components.
- File:
-
Fine-grained dependency tracking for expressions (impact: high)
- Files:
src/template/binding-proxy-handler.ts,src/template/binding-context.ts - You currently track dependencies at property get-time, which is great. Extend
QueuedTasksto batch by binding and collect which keys were read in that binding during last render. On set, only schedule bindings if the changed key intersects the read set. Today, any get registers the binding, but coalescing across keys per binding can reduce redundant renders.
- Files:
-
Scheduler with priority and frame budget (impact: medium–high)
- File:
src/template/binding-proxy-handler.ts - Replace
queueMicrotaskwith a scheduler that can yield to the browser (e.g.,requestAnimationFrame,scheduler.postTaskwhen available). Coalesce microtask bursts into the next frame, process work in chunks to keep frames under 16ms. Useful when many bindings update at once.
- File:
-
Precompiled expression functions per descriptor (impact: medium)
- File:
src/template/binding-context.ts - Currently you cache by
js + paramKeys. You can push compilation to template-compile time: each binding descriptor can prepare a function that, given aBindingContext, reads parameters and evaluates without building strings. For text bindings, pre-split literals and interpolations into a small function. This eliminates repeated string building andnew Functioncalls entirely at runtime.
- File:
-
Avoid
with(this)by generating lexical accessors (impact: high for engines that deopt withwith)- File:
src/template/binding-context.ts - Generate expressions like
(ctx) => { const {user} = ctx.rootModel; ... }or build a safe evaluator that resolves identifiers through a prepared scope object; this reduces deopt risk and often improves inlined performance.
- File:
-
Two-way binding normal form with directive-level handlers (impact: medium)
- File:
src/template/bindings/property-binding-descriptor.ts - Instead of scanning input types in code, standardize directives
value.bind,checked.bind, and register directive handlers per element type. During compile, attach the handler once; at runtime, handlers are direct, no branching required.
- File:
-
Renderer lifecycle and connection once semantics (impact: medium)
- Files:
template-renderer.ts, descriptors - Ensure
connect()is idempotent and only runs when actually needed. For repeaters with stable item renderers, connect child bindings once when they are created; moving nodes should not trigger re-connect.
- Files:
-
DOM range manager for repeaters (impact: medium)
- File:
src/template/bindings/repeater-binding-descriptor.ts - Instead of ad-hoc
Rangecreation, keep a small range manager per repeater holding start (anchor) and an ordered list of end nodes. Provide methods to remove a span, insert before index, and move an item by index. This encapsulation reduces errors and allows optimizing browser operations (e.g., useNode.insertBeforewith fragments; avoidreplaceChildren).
- File:
-
Lazy connect of event bindings (impact: low–medium)
- File:
src/template/bindings/event-binding-descriptor.ts - If many event handlers exist but users don’t interact often, you can lazily attach on first render or the first time the element is visible/intersecting. Alternatively, delegate events at container level where appropriate.
- File:
-
Memory safety: cleanup binding maps on unmount (impact: medium)
- Files:
binding-proxy-handler.ts,bindings/* - When a binding is destroyed (e.g., repeater shrink), ensure its references are removed from the target’s bindings map. Provide a
dispose()onBindingthat unregisters from all tracked keys to prevent memory growth.
- Files:
-
SSR/Pre-render aware compilation (impact: medium)
- File:
template-compilation.ts - Allow precompiling templates to JS structures once at build-time to avoid runtime DOM parsing (
template.innerHTML→DocumentFragment). This changes deployment but eliminates work at startup.
- File:
-
Static analysis of
.bind/text expressions to collect dependencies (impact: medium–high)- File:
binding-context.ts - Instead of relying solely on proxy get-tracking at runtime, parse expressions at compile-time to find identifiers/property paths. Use this to pre-register dependencies and to build optimal watchers; fallback to runtime tracking for dynamic paths.
- File:
-
Smarter batching of repeater growth (impact: medium)
- File:
src/template/bindings/repeater-binding-descriptor.ts - When adding many items, create a single
DocumentFragment, instantiate allBindings into it, and insert once. Also defer.render()calls until after append when possible to leverage off-DOM updates.
- File:
-
Immutable update hints (impact: medium)
- Files:
binding-proxy-handler.ts, public API - If users opt into immutable patterns (replace array rather than mutate), accept hints to skip per-index scheduling and perform a keyed diff directly. Helps very large list updates.
- Files:
-
Configurable change detection granularity (impact: low–medium)
- Provide options per binding: strict identity vs deep-equals for values. E.g.,
text.bindwith primitives can skip updates using===, objects may use identity check. Already present in some places, but make it consistent and configurable.
- Provide options per binding: strict identity vs deep-equals for values. E.g.,
Why these help and rough priorities
- Highest impact for typical apps with lists: keyed diffing + deduped/batched renders in the proxy scheduler. These directly reduce DOM ops which dominate cost.
- Next tier:
BindingContextallocation cuts and removal ofwith(this)deopts. These affect every expression render. - Then: repeater DOM range management and safer clear paths; property-compile optimizations; cleanup to avoid memory leaks.
If you want, I can draft concrete diffs for the most impactful items first: 1) dedupe/batch in BindingProxyHandler, 2) keyed repeater with stable instances, 3) BindingContext array reuse and cache-key memoization.