Table of Contents
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 fine‑grained updates.
Evidence:
- README shows setting up a component and using
value.bind,if.bind, and interpolation${...}. - Core bootstrap at
src/core/eliora.tsregisters components and router view. - Rendering pipeline in
src/component/view-model.tsandsrc/template/*.
Current architecture overview
High-level flow (files referenced):
-
Component registration and bootstrap
Eliora(src/core/eliora.ts) registers a component module, defines a custom element, and exposes template modules via Vite’simport.meta.glob.- When a custom element is used,
WebComponent(extending HTMLElement) andElioraComponent(src/component/eliora-component.ts) resolve the view model, load the template, and mount the view.
-
Template compilation and binding descriptors
TemplateCompilation(src/template/template-compilation.ts): parses string HTML into aDocumentFragmentonce, 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
- Repeaters:
- It separates repeater descriptors from others to handle structural bindings first.
-
Rendering and change propagation
ViewModelconstructs aBindingContextandTemplateRenderer, callsbind()(connect descriptors, render initial), and schedules renders viaqueueMicrotask(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.
- Calls
Binding(src/template/bindings/binding.ts) wraps a descriptor and setsBinding.currentBindingduringrender()so the proxy can record dependencies.
-
Dependency tracking & scheduling
BindingProxyHandler(src/template/binding-proxy-handler.ts) wraps models in aProxyand:- 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 pushingQueuedTask(binding, RenderingContext(target, key)). There’s a placeholderoptimize()method (currently commented out call) intended to coalesce array-length changes.
- During
-
Expression evaluation
BindingContext(src/template/binding-context.ts) supports evaluating binding expressions withnew Functionandwith(this), caching compiled functions inevaluationFunctionCache. It builds an evaluation parameter list (scoped variables for repeater items) and a joined cache key to avoid recompilation.
-
Routing
router-viewis a custom element (router/router-view-component.ts) that wires aRouterinstance to a component and swaps routes by creating child components.router-href-binding-descriptor.tsattaches click handlers to navigate using the nearestRouterlocated 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
ViewModeland 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,attachedhooks;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 Functionandwith(this): security hazards and performance pitfalls, harder to minify/optimize;withprevents 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 aren’t 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.hasContextEqualsTologs proxied values on comparison; logging in render paths can degrade performance.
Architecture improvement proposals (with impact estimates)
- Replace
with(this)expression evaluation with a small expression compiler or scoped evaluator (High)
- Problem:
with(this)andnew Functionharm 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)), nowith. 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.
- 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
*.htmltemplates 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.
- 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.
- Centralized scheduler and render queue (Medium)
- Problem: Simultaneous batching in both
ViewModeland 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,
requestAnimationFramefor visual updates. - Impact: Medium. Reduces duplicate work; clearer boundaries between state changes and DOM flush.
- Binding lifecycle and cleanup (Medium)
- Problem: Stored dependencies in
target[bindingsSymbol]grow; no cleanup on unmount. - Proposal: Add
unbind()anddetached()phases to descriptors andBinding, and ensure unsubscription/cleanup: remove binding references from the target’s map. Consider WeakRef/FinalizationRegistry for safety. - Impact: Medium. Prevents leaks and stale updates.
- Optimize dependency storage and lookup (Low → Medium)
- Problem: Arrays of bindings per key cause O(n) includes and duplicates.
- Proposal: Use
Set<Binding>instead ofBinding[]. Also store a reverse index on the Binding to allow O(1) cleanup from the model’s map. - Impact: Low/Medium. Faster hot-path operations and reliable deduplication.
- 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 aren’t 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.
- Decouple renderer from evaluation context prep (Low)
- Problem:
prepareParametersForEvaluation()mutates state onBindingContextpre-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.
- Feature flags for logs and groups (Low)
- Problem: Hot-path logging (
logger.group,Binding.hasContextEqualsToconsole 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)
- Coalesce array mutations and length updates (Medium)
- Today:
BindingProxyHandler.optimize()exists but is not used;seton array keys includinglengthenqueues 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, thenlength. For repeaters, detect common ops (push/pop/shift/splice) and patch DOM incrementally. - Impact: Medium. Noticeable on list updates.
- Cache and reuse Text/Attr nodes in descriptors (Medium)
- Today: Descriptors likely query and touch DOM per render.
- Do: On
bind(), capture the exactTextorAttrnode references and update their.dataor.valuefields directly. AvoidinnerHTML/template reparsing. - Impact: Medium. Reduces DOM traversal and GC pressure.
- 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.
- 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.
- Expression memoization for stable inputs (Low)
- Keep
evaluationFunctionCache, but also memoize results for pure expressions when their dependency keys didn’t change since last render (you already track dependencies). Store last value and dependency version per binding. - Impact: Low. Cuts CPU on frequent identical renders.
- Reduce microtask churn by grouping (Low → Medium)
- Instead of pushing one
QueuedTaskper binding per key set, group them by binding and squash duplicates within the same microtask. AMap<Binding, Set<RenderingContext>>is enough. - Impact: Low/Medium. Less scheduling overhead.
- Reconcile repeaters via DocumentFragment + Range (Medium)
- When adding/removing/moving items, use
DocumentFragmentandRangeto 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.
- Build-time and bundling (Low)
- Vite/Rollup options: enable aggressive treeshake/inline, ensure production builds strip
console.*, defineprocess.env.NODE_ENV. - Use
esbuildtarget aligned with browsers to reduce polyfills. LeveragemoduleSideEffects: falsein package when true. - Impact: Low. Smaller, faster bundles.
- Memory and GC hygiene (Low)
- Ensure descriptors remove references to nodes and contexts on unbind/detach.
- Avoid capturing large closures unnecessarily in
new Functionstrings; move to compiled functions with stable closures. - Impact: Low. Reduces leaks and long-tail perf issues.
- 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 viaLoggerand optionally integrate with DevTools timeline. - Impact: Low. Helps find hotspots.
Suggested prioritized next steps
- Implement keyed diffing in repeater descriptors and re-enable array update coalescing (Medium/High).
- Introduce a centralized render scheduler that flushes once per view per microtask and optionally switches to
requestAnimationFramefor DOM writes (Medium). - Add binding cleanup on detach/unbind; swap arrays to sets for dependency maps (Medium).
- Remove
with(this)by compiling expressions to functions evaluated over explicit context; keep theevaluationFunctionCache, but keyed by expression string and descriptor ID (High). Consider a simple parser for member access and calls. - 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 1–3 with code sketches for the repeater diffing, scheduler interface, and dependency map changes, or review specific binding descriptor files to tailor the suggestions further.