1 Performance Optimizations
Arnaud Fonce edited this page 2025-10-29 11:55:09 +00:00
This file contains ambiguous Unicode characters

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, and preBuiltParameterValues every render and even duplicates work when a parent exists (lines ~87105). 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.
  • Memoize preBuiltParameterKeys.join(",") for eval cache (impact: lowmedium)

    • File: src/template/binding-context.ts
    • eval() builds cacheKey = this.preBuiltParameterKeys.join(",") + js; on every evaluation (lines ~115119). 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.
  • 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). with impairs JS engine optimizations. If feasible, generate functions that explicitly reference the required identifiers via parameters and avoid with. If keeping with, restrict usage to less frequent code paths.
  • Deduplicate queued renders before executing (impact: high for burst updates)

    • File: src/template/binding-proxy-handler.ts
    • set enqueues a task per binding without dedupe (lines ~6884). If the same binding/key is set multiple times in a microtask, you render multiple times. Maintain a Map<Binding, Set<string>> (or combined key) in QueuedTasks so each binding/key pair renders once per microtask. Call once per binding with a single coalesced RenderingContext or loop unique keys.
  • Reinstate and improve microtask optimization for array ops (impact: mediumhigh for large lists)

    • File: src/template/binding-proxy-handler.ts
    • optimize() pushes length updates 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.
  • Use WeakMap for per-target bindings to prevent leaks (impact: medium)

    • File: src/template/binding-proxy-handler.ts
    • bindingsSymbol map is stored on the target object. For objects not under your control this can retain references. Consider a WeakMap<object, Map<PropertyKey, Binding[]>> owned by the handler, or ensure cleanup when a binding is destroyed.
  • Fast path skip for attribute-less nodes in compile (impact: lowmedium)

    • File: src/template/template-compilation.ts
    • In compileNode(), each descriptors compile inspects attributes for every node (lines ~6070). Add quick guards: if !(node instanceof HTMLElement) skip attribute-based compilers; if node instanceof Text && !includes("${") skip text binding compile call. This reduces redundant checks.
  • 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 length for (let i = 0, n = arr.length; i < n; i++) in hot paths. Minor but measurable in tight render loops.
  • 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 ~2529). This enumerates many props. Prefer checking direct presence if (targetPropName in node) and use a small lowercase-to-actual-name cache per tagName to avoid repeated enumeration.
  • Avoid clearing entire parent for empty repeaters (impact: medium)

    • File: src/template/bindings/repeater-binding-descriptor.ts
    • When newContexts.length <= 0 it calls parent.replaceChildren() then re-appends the anchor (lines 7277). This nukes all siblings. Safer and faster: delete the range that covers only repeated nodes and leave the anchor untouched (create range from commentNode.nextSibling to last rendered node; delete if present).
  • Batch DOM insertion using a single DocumentFragment (impact: medium)

    • File: src/template/bindings/repeater-binding-descriptor.ts
    • Currently creates newNodes = template.newTemplateElements(...) and then commentNode.before(...newNodes) (line 69). Better: append those nodes to a single DocumentFragment and 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.
  • Minimize Range creation in repeater shrink (impact: lowmedium)

    • File: src/template/bindings/repeater-binding-descriptor.ts
    • Each shrink creates a new Range (lines 7983). Reuse a single Range instance stored in renderData or use node removal via nextSibling loop for fewer allocations.
  • Early return in renderDescriptor for repeaters when nothing changed (impact: medium)

    • File: src/template/bindings/repeater-binding-descriptor.ts
    • If newContexts.length === renderData.renderers.length and renderingContext doesnt target the repeaters modelName, skip. You already short-circuit on renderingContext.key === modelName for updates (lines 5459), but you still run creation/deletion checks. Introduce a quick equality check to exit early.
  • Avoid cloning an entire HTMLElement just to extract outerHTML (impact: lowmedium)

    • File: src/template/bindings/repeater-binding-descriptor.ts
    • tplNode = node.cloneNode(true) as HTMLElement; tplNode.attributes.removeNamedItem("repeat.for"); ... tplNode.outerHTML (lines ~2833). You can read the original outerHTML and remove repeat.for via string replacement at compile time, or better, clone minimally and set innerHTML string. Reduces DOM work during compile.
  • Use Text.nodeValue assignment sparingly (impact: low)

    • File: src/template/bindings/text-binding-descriptor.ts
    • Already guarded by last value (2226). Additionally, call textNode.nodeValue = val vs. data is equivalent; keep as is, but ensure val is string to avoid implicit coercion costs in hot paths.
  • 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 3840). Store the listener function and reuse; you already store lastEventListener, but the wrapper arrow still allocates. Keep a bound function and use it directly.
  • Use DocumentFragment for root binding append (impact: low)

    • File: src/template/template-renderer.ts
    • appendFunction(this.bindings.element) is fine. Ensure upstream appendFunction typically appends a fragment or uses batching to avoid layout thrash.

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.
  • 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.
  • 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 QueuedTasks to 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.
  • Scheduler with priority and frame budget (impact: mediumhigh)

    • File: src/template/binding-proxy-handler.ts
    • Replace queueMicrotask with a scheduler that can yield to the browser (e.g., requestAnimationFrame, scheduler.postTask when available). Coalesce microtask bursts into the next frame, process work in chunks to keep frames under 16ms. Useful when many bindings update at once.
  • 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 a BindingContext, reads parameters and evaluates without building strings. For text bindings, pre-split literals and interpolations into a small function. This eliminates repeated string building and new Function calls entirely at runtime.
  • Avoid with(this) by generating lexical accessors (impact: high for engines that deopt with with)

    • 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.
  • 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.
  • 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.
  • DOM range manager for repeaters (impact: medium)

    • File: src/template/bindings/repeater-binding-descriptor.ts
    • Instead of ad-hoc Range creation, 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., use Node.insertBefore with fragments; avoid replaceChildren).
  • Lazy connect of event bindings (impact: lowmedium)

    • File: src/template/bindings/event-binding-descriptor.ts
    • If many event handlers exist but users dont 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.
  • 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 targets bindings map. Provide a dispose() on Binding that unregisters from all tracked keys to prevent memory growth.
  • 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.innerHTMLDocumentFragment). This changes deployment but eliminates work at startup.
  • Static analysis of .bind/text expressions to collect dependencies (impact: mediumhigh)

    • 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.
  • Smarter batching of repeater growth (impact: medium)

    • File: src/template/bindings/repeater-binding-descriptor.ts
    • When adding many items, create a single DocumentFragment, instantiate all Bindings into it, and insert once. Also defer .render() calls until after append when possible to leverage off-DOM updates.
  • 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.
  • Configurable change detection granularity (impact: lowmedium)

    • Provide options per binding: strict identity vs deep-equals for values. E.g., text.bind with primitives can skip updates using ===, objects may use identity check. Already present in some places, but make it consistent and configurable.

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: BindingContext allocation cuts and removal of with(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.