1 Performence Optimization v2
Arnaud Fonce edited this page 2025-10-29 21:48:02 +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.

What this project is and its purpose

Eliora is a lightweight front-end JavaScript framework inspired by Aurelia and designed to work with Vite. It provides:

  • Declarative HTML templates with bindings (text/property/event/conditional/repeater) and a small router (router-view, router-href).
  • A minimal DI container for constructing component view models.
  • A DOM-first rendering approach with a dependency-tracking proxy to schedule finegrained updates.

Evidence:

  • README shows setting up a component and using value.bind, if.bind, and interpolation ${...}.
  • Core bootstrap at src/core/eliora.ts registers components and router view.
  • Rendering pipeline in src/component/view-model.ts and src/template/*.

Current architecture overview

High-level flow (files referenced):

  1. Component registration and bootstrap

    • Eliora (src/core/eliora.ts) registers a component module, defines a custom element, and exposes template modules via Vites import.meta.glob.
    • When a custom element is used, WebComponent (extending HTMLElement) and ElioraComponent (src/component/eliora-component.ts) resolve the view model, load the template, and mount the view.
  2. Template compilation and binding descriptors

    • TemplateCompilation (src/template/template-compilation.ts): parses string HTML into a DocumentFragment once, then walks nodes and compiles them into binding descriptors:
      • Repeaters: repeater-binding-descriptor.ts
      • Conditionals: conditional-binding-descriptor.ts
      • Properties: property-binding-descriptor.ts
      • Text nodes: text-binding-descriptor.ts
      • Events: event-binding-descriptor.ts
      • Router href: router/route-href-binding-descriptor.ts
    • It separates repeater descriptors from others to handle structural bindings first.
  3. Rendering and change propagation

    • ViewModel constructs a BindingContext and TemplateRenderer, calls bind() (connect descriptors, render initial), and schedules renders via queueMicrotask (render(firstRendering)), invoking lifecycle hooks (created, bind, attached).
    • TemplateRenderer (src/template/template-renderer.ts) binds descriptors, then on render:
      • Calls context.prepareParametersForEvaluation() to pre-build expression parameters.
      • Renders repeaters first, then other bindings.
    • Binding (src/template/bindings/binding.ts) wraps a descriptor and sets Binding.currentBinding during render() so the proxy can record dependencies.
  4. Dependency tracking & scheduling

    • BindingProxyHandler (src/template/binding-proxy-handler.ts) wraps models in a Proxy and:
      • During get, if a binding is currently rendering, records that the binding depends on the accessed key (bindingsSymbol: Map<key, Binding[]>). Nested objects are recursively proxied (except Router/Eliora instances).
      • During set, schedules per-binding re-renders in a microtask by pushing QueuedTask(binding, RenderingContext(target, key)). Theres a placeholder optimize() method (currently commented out call) intended to coalesce array-length changes.
  5. Expression evaluation

    • BindingContext (src/template/binding-context.ts) supports evaluating binding expressions with new Function and with(this), caching compiled functions in evaluationFunctionCache. It builds an evaluation parameter list (scoped variables for repeater items) and a joined cache key to avoid recompilation.
  6. Routing

    • router-view is a custom element (router/router-view-component.ts) that wires a Router instance to a component and swaps routes by creating child components.
    • router-href-binding-descriptor.ts attaches click handlers to navigate using the nearest Router located via DI hierarchy.

Key characteristics:

  • DOM-first rendering: no virtual DOM; bindings update live nodes directly.
  • Fine-grained reactivity: bindings re-render individually based on observed model property access while rendering.
  • Microtask batching: both ViewModel and proxy schedule work in microtasks to avoid redundant sync reflows.

Architectural strengths

  • Clear separation: compilation (descriptor discovery) vs rendering (descriptor execution and context updates).
  • Fine-grained updates: dependency tracking per property → avoids full template re-renders.
  • Reasonable lifecycle model: created, bind, attached hooks; bind() clears DOM and mounts fragment once.
  • Router and DI are small and unobtrusive; router-href integrates via a binding descriptor.

Potential pain points and risks

  • Expression evaluation with new Function and with(this): security hazards and performance pitfalls, harder to minify/optimize; with prevents some JS engine optimizations.
  • Dependency graph is per-binding and stored on model proxies using arrays; no cleanup shown on unmount → potential memory leaks if bindings arent removed from maps when a component unbinds/detaches.
  • Array changes optimization disabled: optimize() exists but not used; repeated sets on arrays could cause N redundant renders.
  • Repeaters diffing strategy unclear: likely rebuilds region on change unless descriptors implement keyed diffing; risks expensive DOM churn for collections.
  • Batching strategy duplicated: ViewModel.render() batches, and proxy also batches. This can lead to double scheduling and wasted work if both trigger rerenders of the same bindings.
  • Context preparation cost: prepareParametersForEvaluation() does array allocations each render call on renderer; depending on frequency, that can add overhead.
  • Logging in hot path: Binding.hasContextEqualsTo logs proxied values on comparison; logging in render paths can degrade performance.

Architecture improvement proposals (with impact estimates)

  1. Replace with(this) expression evaluation with a small expression compiler or scoped evaluator (High)
  • Problem: with(this) and new Function harm performance and debuggability, and raise security flags. Cache keys depend on concatenated strings which can explode combinatorially.
  • Proposal: Introduce a tiny expression parser or adopt a safe expression library to compile expressions into property-path accessors. Evaluate against a context object explicitly (fn(context)), no with. Persist compiled AST/functions per binding descriptor at compile time.
  • Impact: High. Better performance, safer evaluation, improved minifier friendliness. Requires descriptor changes and migration of BindingContext.
  1. AOT template compilation via Vite plugin (High)
  • Problem: Runtime DOM parsing and descriptor discovery costs on first render.
  • Proposal: Provide a Vite plugin that reads *.html templates at build-time and emits precompiled descriptor arrays and static node blueprints (or even factories that produce cloned fragments). This mirrors what many frameworks do to remove template parsing from runtime.
  • Impact: High. Large startup gains; enables dead-code elimination around unused descriptor types.
  1. Keyed repeaters with diffing (Medium → High depending on scope)
  • Problem: Without diff, any mutation can cause full teardown/rebuild for list regions. No visible key support.
  • Proposal: Support repeat.for="item of items; track by item.id" or implicit identity tracking. Implement a minimal keyed diff (e.g., LIS-based or simple keyed map reconciliation) to only insert/move/remove nodes needed.
  • Impact: Medium to High. Major wins for lists, more complex to implement but localized to repeater descriptors.
  1. Centralized scheduler and render queue (Medium)
  • Problem: Simultaneous batching in both ViewModel and proxy; per-binding microtasks can cause thrashing.
  • Proposal: Introduce a central scheduler that groups render jobs by component/view. Proxy enqueues keys per component; the scheduler coalesces and flushes once per microtask or animation frame. Allow policy: microtask for data correctness, requestAnimationFrame for visual updates.
  • Impact: Medium. Reduces duplicate work; clearer boundaries between state changes and DOM flush.
  1. Binding lifecycle and cleanup (Medium)
  • Problem: Stored dependencies in target[bindingsSymbol] grow; no cleanup on unmount.
  • Proposal: Add unbind() and detached() phases to descriptors and Binding, and ensure unsubscription/cleanup: remove binding references from the targets map. Consider WeakRef/FinalizationRegistry for safety.
  • Impact: Medium. Prevents leaks and stale updates.
  1. Optimize dependency storage and lookup (Low → Medium)
  • Problem: Arrays of bindings per key cause O(n) includes and duplicates.
  • Proposal: Use Set<Binding> instead of Binding[]. Also store a reverse index on the Binding to allow O(1) cleanup from the models map.
  • Impact: Low/Medium. Faster hot-path operations and reliable deduplication.
  1. Formalize component scope DI and router hierarchy (Low)
  • Problem: Router lookup climbs the DOM to find a DI scope (see route-href-binding-descriptor.ts); DI scoping rules arent explicit.
  • Proposal: Add explicit hierarchical injectors per component with clear parent/child relationships, making router lookup O(1) by scope.
  • Impact: Low. Clarifies ownership and reduces DOM walking.
  1. Decouple renderer from evaluation context prep (Low)
  • Problem: prepareParametersForEvaluation() mutates state on BindingContext pre-render, which couples render timing and evaluation.
  • Proposal: Let each descriptor provide a statically-known parameter list; move prep to bind time and only update on context changes.
  • Impact: Low. Minor structural clarity and fewer allocations per render.
  1. Feature flags for logs and groups (Low)
  • Problem: Hot-path logging (logger.group, Binding.hasContextEqualsTo console logs).
  • Proposal: Compile-time or runtime flags to remove or noop logs in production.
  • Impact: Low. Direct perf gain in prod builds.

Low-level performance optimization proposals (with impact estimates)

  1. Coalesce array mutations and length updates (Medium)
  • Today: BindingProxyHandler.optimize() exists but is not used; set on array keys including length enqueues many tasks.
  • Do: Re-enable and improve optimize() to collapse multiple array key updates into a single render task per collection, preferably rendering in order: mutations, then length. For repeaters, detect common ops (push/pop/shift/splice) and patch DOM incrementally.
  • Impact: Medium. Noticeable on list updates.
  1. Cache and reuse Text/Attr nodes in descriptors (Medium)
  • Today: Descriptors likely query and touch DOM per render.
  • Do: On bind(), capture the exact Text or Attr node references and update their .data or .value fields directly. Avoid innerHTML/template reparsing.
  • Impact: Medium. Reduces DOM traversal and GC pressure.
  1. Avoid layout thrash (Low → Medium)
  • Do: During render, never read layout (offsetWidth/Height, getBoundingClientRect) after writes in the same frame. If needed, batch reads first, writes second, or move visual flushes to requestAnimationFrame.
  • Impact: Low/Medium depending on use cases.
  1. Event handling improvements (Low)
  • Use event delegation for lists: attach a single handler at container level for homogeneous child events.
  • Mark passive handlers for scroll/touch ({ passive: true }).
  • Ensure handler references are stable to prevent re-attachment on each render.
  • Impact: Low. Smoother input/scrolling.
  1. Expression memoization for stable inputs (Low)
  • Keep evaluationFunctionCache, but also memoize results for pure expressions when their dependency keys didnt change since last render (you already track dependencies). Store last value and dependency version per binding.
  • Impact: Low. Cuts CPU on frequent identical renders.
  1. Reduce microtask churn by grouping (Low → Medium)
  • Instead of pushing one QueuedTask per binding per key set, group them by binding and squash duplicates within the same microtask. A Map<Binding, Set<RenderingContext>> is enough.
  • Impact: Low/Medium. Less scheduling overhead.
  1. Reconcile repeaters via DocumentFragment + Range (Medium)
  • When adding/removing/moving items, use DocumentFragment and Range to do bulk DOM operations (range.deleteContents(), range.insertNode(fragment)).
  • Keep a per-item anchor node to move blocks efficiently.
  • Impact: Medium. Big win for list-heavy UIs.
  1. Build-time and bundling (Low)
  • Vite/Rollup options: enable aggressive treeshake/inline, ensure production builds strip console.*, define process.env.NODE_ENV.
  • Use esbuild target aligned with browsers to reduce polyfills. Leverage moduleSideEffects: false in package when true.
  • Impact: Low. Smaller, faster bundles.
  1. Memory and GC hygiene (Low)
  • Ensure descriptors remove references to nodes and contexts on unbind/detach.
  • Avoid capturing large closures unnecessarily in new Function strings; move to compiled functions with stable closures.
  • Impact: Low. Reduces leaks and long-tail perf issues.
  1. Diagnostics and profiling hooks (Low)
  • Provide a debug flag to collect simple metrics: number of descriptor renders per tick, render durations (performance.mark/measure), queue sizes. Expose via Logger and optionally integrate with DevTools timeline.
  • Impact: Low. Helps find hotspots.

Suggested prioritized next steps

  1. Implement keyed diffing in repeater descriptors and re-enable array update coalescing (Medium/High).
  2. Introduce a centralized render scheduler that flushes once per view per microtask and optionally switches to requestAnimationFrame for DOM writes (Medium).
  3. Add binding cleanup on detach/unbind; swap arrays to sets for dependency maps (Medium).
  4. Remove with(this) by compiling expressions to functions evaluated over explicit context; keep the evaluationFunctionCache, but keyed by expression string and descriptor ID (High). Consider a simple parser for member access and calls.
  5. Optional but strategic: build a Vite plugin for AOT template compilation (High) to shift parsing/discovery to build time.

If you want, I can draft a concrete roadmap focusing on 13 with code sketches for the repeater diffing, scheduler interface, and dependency map changes, or review specific binding descriptor files to tailor the suggestions further.