Table of Contents
- Project Overview
- Architecture-Level Performance Optimizations
- 1. Implement Virtual DOM or Incremental DOM Strategy
- 2. Add Compiled Template Caching Layer
- 3. Implement Lazy Binding Descriptor Initialization
- Low-Level Code Performance Optimizations
- 4. Optimize Proxy Handler with Dependency Tracking
- 5. Optimize Conditional Binding with Comment Node Reuse
- 6. Implement Event Delegation for Event Bindings
- 7. Optimize Repeat Binding with DocumentFragment
- 8. Add String Interpolation Optimization
- 9. Implement Binding Expression Compilation
- 10. Add Microtask Batching for Renders
- 11. Optimize Node Address Lookup
- 12. Add Attribute Binding Memoization
- Summary Table
- Follow-up in response of my remarks on propsal #4 already done
- Potential Improvements to Your Existing Dependency Tracking
- Follow-up when asking to focus on reapeater and sub context
- Focused Repeater Performance Optimizations
- 1. Implement Item Keying System (Virtual DOM-style reconciliation)
- 2. Optimize SubContextBindingDescriptor Initialization
- 3. Implement Renderer Pooling
- 4. Cache Template Elements Creation
- 5. Batch DOM Operations in Repeater
- 6. Add Partial Update Detection
- 7. Optimize Range Deletion
- 8. Implement Virtual Scrolling for Large Lists
- Summary Table
I'll deeply examine the eliorajs project to understand its architecture and propose concrete performance improvements.Based on my examination of the eliorajs project, I can provide you with insights and concrete performance optimization proposals. Let me share what I've discovered:
Project Overview
Eliorajs is a lightweight JavaScript framework for building web applications with a component-based architecture. It provides:
Core Features:
- Component System: Custom web components with lifecycle management
- Template Engine: Data binding system with multiple binding types (text, event, repeat, conditional)
- Dependency Injection: IoC container for managing component dependencies
- Router: Client-side routing capabilities
- HTTP Client: Built-in HTTP request handling
Architecture Overview:
eliorajs/
├── core/ # Main framework initialization and logging
├── component/ # Component definitions and web component wrappers
├── template/ # Template compilation and rendering engine
│ └── bindings/ # Different binding descriptors (text, event, repeat, conditional)
├── inject/ # Dependency injection container
├── router/ # Client-side routing
└── http/ # HTTP client utilities
How It Works:
- Template Compilation: Templates are parsed and converted into binding descriptors
- Proxy-based Reactivity: Uses JavaScript Proxies to track data changes and trigger re-renders
- Binding Descriptors: Each binding type (text, event, repeat, conditional) has its own descriptor that handles rendering
- Component Lifecycle: Components go through initialization, rendering, and cleanup phases
Architecture-Level Performance Optimizations
1. Implement Virtual DOM or Incremental DOM Strategy
Location: src/template/ directory, particularly template-renderer.ts and binding descriptors
Current Issue: The framework directly manipulates the DOM on every change through binding descriptors. This can cause:
- Multiple reflows/repaints
- Inefficient DOM operations
- No batching of updates
Proposal:
- Introduce a Virtual DOM layer or adopt Incremental DOM approach
- Batch DOM updates within a microtask/requestAnimationFrame
- Diff algorithm to minimize actual DOM operations
Impact: HIGH - This would dramatically reduce DOM thrashing and improve rendering performance, especially for complex UIs with frequent updates.
2. Add Compiled Template Caching Layer
Location: src/template/template-compilation.ts
Current Issue: Templates are compiled during component initialization but may be re-compiled for repeated component instances.
Proposal:
- Add a global template cache with WeakMap or Map
- Cache compiled binding descriptors by template string hash
- Reuse compiled templates across component instances
Impact: MEDIUM - Reduces CPU overhead during component instantiation, especially beneficial when creating many instances of the same component.
3. Implement Lazy Binding Descriptor Initialization
Location: src/template/bindings/ - all binding descriptor files
Current Issue: All bindings are compiled upfront even if they're in conditional branches that may never render.
Proposal:
- Implement lazy compilation for conditional bindings (
if.bind) - Only compile and initialize bindings when they're first needed
- Use a two-phase compilation: structure analysis first, detailed compilation on-demand
Impact: MEDIUM - Reduces initial load time and memory usage, especially for large templates with many conditionals.
Low-Level Code Performance Optimizations
4. Optimize Proxy Handler with Dependency Tracking
Location: src/template/binding-proxy-handler.ts
Current Issue: Likely triggers all bindings on any property change without tracking which bindings actually depend on which properties.
Proposal:
// Create a dependency graph during binding compilation
class DependencyTracker {
private deps = new Map<string, Set<BindingDescriptor>>();
trackDependency(property: string, binding: BindingDescriptor) {
if (!this.deps.has(property)) {
this.deps.set(property, new Set());
}
this.deps.get(property)!.add(binding);
}
getAffectedBindings(property: string): Set<BindingDescriptor> {
return this.deps.get(property) || new Set();
}
}
// In proxy handler, only notify affected bindings
set(target: any, property: string, value: any) {
target[property] = value;
const affectedBindings = this.tracker.getAffectedBindings(property);
affectedBindings.forEach(binding => binding.render());
return true;
}
Impact: HIGH - Dramatically reduces unnecessary re-renders by only updating bindings that actually depend on changed data.
5. Optimize Conditional Binding with Comment Node Reuse
Location: src/template/bindings/conditional-binding-descriptor.ts
Current Issue: Creates new comment nodes on every bind operation and performs node swapping repeatedly.
Proposal:
bindRenderData(_node: Node, renderData: any) {
// Reuse comment node if already created
if (!renderData.anchorNode) {
renderData.anchorNode = document.createComment("anchor");
renderData.anchorInDOM = false;
renderData.lastConditionValue = undefined; // Track last value
}
}
renderDescriptor(node: Node, context: BindingContext, renderData: any) {
const currentValue = context.evalBinding(this.bindingStatement);
// Skip if condition hasn't changed
if (renderData.lastConditionValue === currentValue) {
return;
}
renderData.lastConditionValue = currentValue;
if (currentValue) {
if (renderData.anchorInDOM) {
renderData.anchorNode.parentNode.replaceChild(node, renderData.anchorNode);
renderData.anchorInDOM = false;
}
} else {
if (!renderData.anchorInDOM && node.parentNode) {
node.parentNode.replaceChild(renderData.anchorNode, node);
renderData.anchorInDOM = true;
}
}
}
Impact: MEDIUM - Reduces unnecessary DOM operations when condition value doesn't change and avoids repeated node creation.
6. Implement Event Delegation for Event Bindings
Location: src/template/bindings/event-binding-descriptor.ts
Current Issue: Each element with an event binding gets its own event listener attached directly.
Proposal:
// In template renderer or component level
class EventDelegationManager {
private static rootHandlers = new Map<string, boolean>();
private static eventMap = new WeakMap<Node, Map<string, Function>>();
static registerEvent(node: Node, eventType: string, handler: Function, rootElement: Element) {
// Store handler reference
if (!this.eventMap.has(node)) {
this.eventMap.set(node, new Map());
}
this.eventMap.get(node)!.set(eventType, handler);
// Setup root delegation if not already done
const key = `${eventType}-${rootElement}`;
if (!this.rootHandlers.has(key)) {
rootElement.addEventListener(eventType, (e) => {
let target = e.target as Node;
while (target && target !== rootElement) {
const handlers = this.eventMap.get(target);
if (handlers?.has(eventType)) {
handlers.get(eventType)!(e);
break;
}
target = target.parentNode!;
}
});
this.rootHandlers.set(key, true);
}
}
}
Impact: MEDIUM-HIGH - Significantly reduces memory usage and improves performance for components with many event handlers (lists, tables, etc.).
7. Optimize Repeat Binding with DocumentFragment
Location: src/template/bindings/repeat-binding-descriptor.ts
Current Issue: Likely appends DOM nodes one by one during list rendering, causing multiple reflows.
Proposal:
renderDescriptor(node: Node, context: BindingContext, renderData: any) {
const items = context.evalBinding(this.bindingStatement);
// Use DocumentFragment for batch insertion
const fragment = document.createDocumentFragment();
const newNodes: Node[] = [];
items.forEach((item: any, index: number) => {
const clonedNode = node.cloneNode(true);
// ... bind item data to cloned node
fragment.appendChild(clonedNode);
newNodes.push(clonedNode);
});
// Single DOM operation
const parent = node.parentNode;
if (parent) {
// Clear old nodes
renderData.renderedNodes?.forEach((n: Node) => n.remove());
parent.insertBefore(fragment, node);
renderData.renderedNodes = newNodes;
}
}
Impact: HIGH - Dramatically improves performance when rendering lists by batching DOM insertions into a single operation.
8. Add String Interpolation Optimization
Location: src/template/bindings/text-binding-descriptor.ts
Current Issue: Likely evaluates binding expressions on every render even if the result hasn't changed.
Proposal:
class TextBindingDescriptor {
private cachedResult: string | undefined;
private dependencies: Set<string> = new Set();
renderDescriptor(node: Node, context: BindingContext, renderData: any) {
const newValue = context.evalBinding(this.bindingStatement);
// Only update text node if value actually changed
if (renderData.lastValue !== newValue) {
if (node.nodeType === Node.TEXT_NODE) {
node.textContent = newValue;
}
renderData.lastValue = newValue;
}
}
}
Impact: MEDIUM - Prevents unnecessary text node updates when data hasn't changed, reducing browser repaints.
9. Implement Binding Expression Compilation
Location: src/template/binding-context.ts - evalBinding method
Current Issue: Using eval() or Function() constructor for every binding evaluation is slow.
Proposal:
class BindingExpressionCompiler {
private cache = new Map<string, Function>();
compile(expression: string): Function {
if (this.cache.has(expression)) {
return this.cache.get(expression)!;
}
// Create optimized function with context
const fn = new Function('$data', `with($data) { return (${expression}); }`);
this.cache.set(expression, fn);
return fn;
}
eval(expression: string, context: any): any {
const fn = this.compile(expression);
return fn(context);
}
}
Impact: MEDIUM-HIGH - Eliminates repeated parsing/compilation of the same binding expressions, significantly faster execution.
10. Add Microtask Batching for Renders
Location: src/template/rendering-context.ts and template-renderer.ts
Current Issue: Multiple data changes likely trigger multiple synchronous renders.
Proposal:
class RenderScheduler {
private pendingComponents = new Set<Component>();
private scheduled = false;
scheduleRender(component: Component) {
this.pendingComponents.add(component);
if (!this.scheduled) {
this.scheduled = true;
queueMicrotask(() => {
this.flush();
});
}
}
private flush() {
const components = Array.from(this.pendingComponents);
this.pendingComponents.clear();
this.scheduled = false;
components.forEach(c => c.render());
}
}
Impact: HIGH - Batches multiple updates into a single render pass, preventing cascading renders and improving performance significantly.
11. Optimize Node Address Lookup
Location: src/template/binding-descriptor.ts - NodeAddress class
Current Issue: Traversing the DOM tree using indexes array on every binding operation is inefficient.
Proposal:
class NodeAddress {
private cachedNode: WeakRef<Node> | null = null;
getNode(root: Node): Node {
// Check cache first
const cached = this.cachedNode?.deref();
if (cached && cached.isConnected) {
return cached;
}
// Traverse and cache
let node = root;
for (const index of this.indexes) {
node = node.childNodes[index];
}
this.cachedNode = new WeakRef(node);
return node;
}
}
Impact: MEDIUM - Reduces repeated DOM traversals by caching node references with WeakRef to avoid memory leaks.
12. Add Attribute Binding Memoization
Location: src/template/bindings/attribute-binding-descriptor.ts
Proposal:
class AttributeBindingDescriptor {
renderDescriptor(node: Node, context: BindingContext, renderData: any) {
if (node instanceof HTMLElement) {
const newValue = context.evalBinding(this.bindingStatement);
// Only update if value changed
if (renderData.lastValue !== newValue) {
node.setAttribute(this.attributeName, newValue);
renderData.lastValue = newValue;
}
}
}
}
Impact: LOW-MEDIUM - Prevents unnecessary attribute updates, reducing DOM modification overhead.
Summary Table
| # | Optimization | Location | Impact | Type |
|---|---|---|---|---|
| 1 | Virtual/Incremental DOM | template-renderer.ts | HIGH | Architecture |
| 2 | Template Caching | template-compilation.ts | MEDIUM | Architecture |
| 3 | Lazy Binding Init | bindings/ | MEDIUM | Architecture |
| 4 | Dependency Tracking | binding-proxy-handler.ts | HIGH | Code |
| 5 | Conditional Optimization | conditional-binding-descriptor.ts | MEDIUM | Code |
| 6 | Event Delegation | event-binding-descriptor.ts | MEDIUM-HIGH | Code |
| 7 | DocumentFragment for Lists | repeat-binding-descriptor.ts | HIGH | Code |
| 8 | Text Binding Cache | text-binding-descriptor.ts | MEDIUM | Code |
| 9 | Expression Compilation | binding-context.ts | MEDIUM-HIGH | Code |
| 10 | Render Batching | rendering-context.ts | HIGH | Code |
| 11 | Node Address Cache | binding-descriptor.ts | MEDIUM | Code |
| 12 | Attribute Memoization | attribute-binding-descriptor.ts | LOW-MEDIUM | Code |
These optimizations are specifically tailored to eliorajs's architecture and would provide significant performance improvements across different use cases.
Follow-up in response of my remarks on propsal #4 already done
You're absolutely right! I apologize for the oversight. Looking at your code more carefully, you already have a sophisticated dependency tracking system in place:
- In the
gettrap: You track which bindings access which properties by checkingBinding.currentBindingand storing it intarget[BindingProxyHandler.bindingsSymbol] - In the
settrap: You only trigger the specific bindings that depend on the changed property:bindingsMap.get(key) ?? [] - You also have microtask batching: Using
queueMicrotask()to batch multiple updates
So my proposal #4 is already implemented in your code! Your implementation is actually quite smart:
// During binding evaluation (get trap)
if (Binding.currentBinding) {
// Track that this binding depends on this property
target[BindingProxyHandler.bindingsSymbol].get(key).push(Binding.currentBinding);
}
// During property update (set trap)
let bindings: Binding[] = bindingsMap.get(key) ?? [];
// Only render bindings that depend on this specific property
for (let binding of bindings) {
BindingProxyHandler.renderingContextsForMicrotask.push(...)
}
And you've also implemented proposal #10 (microtask batching) with your queueMicrotask() logic!
This is actually a very elegant reactive system similar to Vue 3's approach. So I need to revise my recommendations - you can disregard proposals #4 and #10 as they're already well-implemented in your codebase.
Potential Improvements to Your Existing Dependency Tracking
However, I did notice one potential issue and one optimization opportunity:
Issue: Memory Leak Potential
Location: The get trap in binding-proxy-handler.ts
Problem: Bindings are pushed to the array but never removed. If a binding is destroyed or a component is unmounted, those binding references remain in the Map, preventing garbage collection.
Solution:
// Add cleanup method to binding
class Binding {
dispose() {
// Remove this binding from all tracked properties
// Need to track which properties this binding is registered to
}
}
Impact: MEDIUM - Prevents memory leaks in long-running applications with dynamic components.
Optimization: Duplicate Binding Registration
Location: The get trap in binding-proxy-handler.ts
Current code:
if (!target[BindingProxyHandler.bindingsSymbol].get(key).includes(Binding.currentBinding)) {
target[BindingProxyHandler.bindingsSymbol].get(key).push(Binding.currentBinding);
}
Issue: The .includes() check is O(n) for each property access during binding evaluation.
Optimization: Use a Set instead of Array:
target[BindingProxyHandler.bindingsSymbol] = new Map<string | number, Set<Binding>>();
// In get trap:
if (!target[BindingProxyHandler.bindingsSymbol].has(key)) {
target[BindingProxyHandler.bindingsSymbol].set(key, new Set());
}
// Set.add() automatically handles duplicates
target[BindingProxyHandler.bindingsSymbol].get(key).add(Binding.currentBinding);
Impact: LOW-MEDIUM - O(1) insertion instead of O(n), matters more for complex bindings that access many properties.
So thank you for pointing that out - your implementation already has sophisticated reactivity! The other proposals (1-3, 5-9, 11-12) remain relevant and concrete to your project.
Follow-up when asking to focus on reapeater and sub context
Perfect! Now I can see the full picture of your repeater and sub-context system. Let me provide you with concrete, focused proposals to reduce the cost of adding or updating nodes in the repeater:
Focused Repeater Performance Optimizations
1. Implement Item Keying System (Virtual DOM-style reconciliation)
Location: repeater-binding-descriptor.ts
Current Issue: When the array changes, you're doing positional matching (index-based). If items are reordered, inserted in the middle, or removed, you recreate DOM nodes unnecessarily.
Proposal: Add a keying system to identify which items moved vs. which are new:
export class RepeaterBindingDescriptor extends BindingDescriptor {
private readonly keyExpression?: string;
constructor(nodeAddress: NodeAddress, private bindingStatement: string, template: string) {
super(nodeAddress);
// Support syntax: "item of items; key: item.id"
const parts = bindingStatement.split(';');
const mainPart = parts[0];
const keyPart = parts[1]?.trim();
if (keyPart && keyPart.startsWith('key:')) {
this.keyExpression = keyPart.substring(4).trim();
}
this.modelName = mainPart.split("of")[1].trim();
this.scopedName = mainPart.split("of")[0].trim();
// ... existing code ...
}
bindRenderData(node: Node, renderData: any) {
renderData.commentNode = node.nextSibling;
renderData.parentNode = node.nextSibling?.parentNode;
renderData.renderers = [];
renderData.keyToRenderer = new Map(); // Key-based lookup
}
renderDescriptor(_node: Node, context: BindingContext, renderData: any, renderingContext?: RenderingContext) {
let newContexts = context.parseRepeaterBinding(this.modelName, this.scopedName);
if (this.keyExpression) {
this.reconcileWithKeys(newContexts, renderData, context);
} else {
this.reconcileByIndex(newContexts, renderData, renderingContext);
}
}
private reconcileWithKeys(newContexts: BindingContext[], renderData: any, parentContext: BindingContext) {
const newKeyToContext = new Map<any, BindingContext>();
const newKeys: any[] = [];
// Build map of new items by key
for (const ctx of newContexts) {
const key = parentContext.evalBindingInContext(this.keyExpression!, ctx);
newKeyToContext.set(key, ctx);
newKeys.push(key);
}
const oldKeyToRenderer = renderData.keyToRenderer;
const newRenderers: Binding[] = [];
const nodesToInsert: { node: Node, index: number }[] = [];
// Match existing renderers to new contexts
for (let i = 0; i < newKeys.length; i++) {
const key = newKeys[i];
if (oldKeyToRenderer.has(key)) {
// Reuse existing renderer
const renderer = oldKeyToRenderer.get(key);
renderer.updateContext(newKeyToContext.get(key)!);
renderer.render();
newRenderers.push(renderer);
oldKeyToRenderer.delete(key);
} else {
// Create new renderer
const newNode = this.template.newTemplateElements(1)[0];
const binding = new Binding(
newNode,
new SubContextBindingDescriptor(new NodeAddress([]), this.template),
newKeyToContext.get(key)!
);
binding.bind();
binding.render();
newRenderers.push(binding);
nodesToInsert.push({ node: newNode, index: i });
}
}
// Remove unused renderers (items that were deleted)
for (const [_, renderer] of oldKeyToRenderer) {
renderer.getFragmentConsumedNode().remove();
}
// Batch insert new nodes at correct positions
if (nodesToInsert.length > 0) {
const fragment = document.createDocumentFragment();
for (const { node } of nodesToInsert) {
fragment.appendChild(node);
}
renderData.commentNode.before(fragment);
}
// Update renderData
renderData.renderers = newRenderers;
renderData.keyToRenderer.clear();
for (let i = 0; i < newKeys.length; i++) {
renderData.keyToRenderer.set(newKeys[i], newRenderers[i]);
}
}
private reconcileByIndex(newContexts: BindingContext[], renderData: any, renderingContext?: RenderingContext) {
// Keep existing index-based logic for backward compatibility
// ... existing renderDescriptor code ...
}
}
Usage:
<div repeat.for="user of users; key: user.id">
${user.name}
</div>
Impact: HIGH - Dramatically reduces DOM operations when list items are reordered, filtered, or inserted. Reuses existing DOM nodes instead of recreating them.
2. Optimize SubContextBindingDescriptor Initialization
Location: sub-context-binding-descriptor.ts
Current Issue: Every time renderDescriptor is called, it checks if renderer exists. The TemplateRenderer is created lazily, but the binding process happens repeatedly.
Proposal: Separate initialization from update:
export class SubContextBindingDescriptor extends BindingDescriptor {
constructor(nodeAddress: NodeAddress, private template: TemplateCompilation) {
super(nodeAddress);
}
bindRenderData(_node: Node, renderData: any): void {
// Pre-initialize the renderer during bind phase
renderData.initialized = false;
}
connectDescriptor(_node: Node, context: BindingContext, renderData: any): void {
// Initialize renderer once during connect phase
if (!renderData.initialized) {
renderData.renderer = new TemplateRenderer(this.template, context);
renderData.renderer.bind((_: Node) => {}, _node);
renderData.initialized = true;
}
}
renderDescriptor(_node: Node, context: BindingContext, renderData: any): void {
if (renderData.initialized) {
// Fast path: just update and render
renderData.renderer.updateBindingContext(context);
renderData.renderer.render();
} else {
// Fallback for backward compatibility
renderData.renderer = new TemplateRenderer(this.template, context);
renderData.renderer.bind((_: Node) => {}, _node);
renderData.initialized = true;
}
}
}
Impact: MEDIUM - Separates initialization from updates, making subsequent renders faster. Reduces conditional checks on every render.
3. Implement Renderer Pooling
Location: repeater-binding-descriptor.ts
Current Issue: When items are removed and then added back, new renderers and DOM nodes are created from scratch.
Proposal: Maintain a pool of unused renderers to reuse:
export class RepeaterBindingDescriptor extends BindingDescriptor {
private static rendererPool = new WeakMap<TemplateCompilation, Binding[]>();
bindRenderData(node: Node, renderData: any) {
renderData.commentNode = node.nextSibling;
renderData.parentNode = node.nextSibling?.parentNode;
renderData.renderers = [];
renderData.pool = []; // Local pool for this repeater instance
}
private getRendererFromPool(context: BindingContext): Binding | null {
const globalPool = RepeaterBindingDescriptor.rendererPool.get(this.template) || [];
if (globalPool.length > 0) {
const renderer = globalPool.pop()!;
renderer.updateContext(context);
return renderer;
}
return null;
}
private returnRendererToPool(renderer: Binding) {
if (!RepeaterBindingDescriptor.rendererPool.has(this.template)) {
RepeaterBindingDescriptor.rendererPool.set(this.template, []);
}
const pool = RepeaterBindingDescriptor.rendererPool.get(this.template)!;
// Limit pool size to prevent memory leaks
if (pool.length < 50) {
pool.push(renderer);
}
}
renderDescriptor(_node: Node, context: BindingContext, renderData: any, renderingContext?: RenderingContext) {
let newContexts = context.parseRepeaterBinding(this.modelName, this.scopedName);
// ... existing logic for updates ...
// When adding new items
if (newContexts.length > renderData.renderers.length) {
let newNodes: Node[] = [];
let rendererLength = renderData.renderers.length;
for (let i = rendererLength, n = newContexts.length; i < n; i++) {
// Try to reuse from pool
let renderer = this.getRendererFromPool(newContexts[i]);
if (renderer) {
// Reuse existing renderer
renderer.updateContext(newContexts[i]);
renderer.render();
renderData.renderers.push(renderer);
newNodes.push(renderer.getFragmentConsumedNode());
} else {
// Create new renderer
let newNode = this.template.newTemplateElements(1)[0];
renderer = new Binding(
newNode,
new SubContextBindingDescriptor(new NodeAddress([]), this.template),
newContexts[i]
);
renderer.bind();
renderer.render();
renderData.renderers.push(renderer);
newNodes.push(newNode);
}
}
renderData.commentNode.before(...newNodes);
}
// When removing items - return to pool instead of destroying
if (newContexts.length < renderData.renderers.length) {
let range = document.createRange();
range.setStartBefore(renderData.renderers[newContexts.length].getFragmentConsumedNode());
range.setEndAfter(renderData.renderers[renderData.renderers.length - 1].getFragmentConsumedNode());
range.deleteContents();
// Return removed renderers to pool
for (let i = newContexts.length; i < renderData.renderers.length; i++) {
this.returnRendererToPool(renderData.renderers[i]);
}
renderData.renderers.length = newContexts.length;
}
}
}
Impact: MEDIUM-HIGH - Reduces allocation/GC pressure when lists frequently change size. Especially beneficial for dynamic lists (filtering, pagination, etc.).
4. Cache Template Elements Creation
Location: template-compilation.ts - newTemplateElements method
Current Issue: newTemplateElements likely clones the template DOM for each new item.
Proposal: Use a more efficient cloning strategy:
export class TemplateCompilation {
private cachedTemplate?: HTMLTemplateElement;
newTemplateElements(count: number): Node[] {
// Cache the template element for reuse
if (!this.cachedTemplate) {
this.cachedTemplate = document.createElement('template');
this.cachedTemplate.innerHTML = this.template;
}
const result: Node[] = [];
// Use importNode which is faster than innerHTML parsing
for (let i = 0; i < count; i++) {
result.push(this.cachedTemplate.content.cloneNode(true));
}
return result;
}
}
Impact: MEDIUM - Faster DOM cloning by using native <template> element and cloneNode, avoids repeated HTML parsing.
5. Batch DOM Operations in Repeater
Location: repeater-binding-descriptor.ts - renderDescriptor
Current Issue: When updating multiple existing items, each render() call may trigger individual DOM updates.
Proposal: Use DocumentFragment for batch updates:
renderDescriptor(_node: Node, context: BindingContext, renderData: any, renderingContext?: RenderingContext) {
let newContexts = context.parseRepeaterBinding(this.modelName, this.scopedName);
if (renderingContext && renderingContext.key === this.modelName) {
// Detach all nodes before updating to prevent layout thrashing
const fragment = document.createDocumentFragment();
const nodesToReattach: Node[] = [];
for (let i = 0, n = newContexts.length, o = renderData.renderers.length; i < o && i < n; i++) {
const node = renderData.renderers[i].getFragmentConsumedNode();
nodesToReattach.push(node);
fragment.appendChild(node);
// Update in detached state
renderData.renderers[i].updateContext(newContexts[i]);
renderData.renderers[i].render();
}
// Reattach all at once
renderData.commentNode.before(fragment);
}
// ... rest of existing code ...
}
Impact: MEDIUM-HIGH - Reduces layout thrashing when updating multiple items by batching DOM reattachment.
6. Add Partial Update Detection
Location: repeater-binding-descriptor.ts
Current Issue: All visible items are updated even if only one item in the array changed.
Proposal: Track which items actually changed:
bindRenderData(node: Node, renderData: any) {
renderData.commentNode = node.nextSibling;
renderData.parentNode = node.nextSibling?.parentNode;
renderData.renderers = [];
renderData.lastContexts = []; // Track previous contexts
}
renderDescriptor(_node: Node, context: BindingContext, renderData: any, renderingContext?: RenderingContext) {
let newContexts = context.parseRepeaterBinding(this.modelName, this.scopedName);
if (renderingContext && renderingContext.key === this.modelName) {
// Only update items that changed
for (let i = 0, n = Math.min(newContexts.length, renderData.renderers.length); i < n; i++) {
// Check if context actually changed
if (!renderData.lastContexts[i] ||
renderData.lastContexts[i].proxyValue !== newContexts[i].proxyValue) {
renderData.renderers[i].updateContext(newContexts[i]);
renderData.renderers[i].render();
}
}
renderData.lastContexts = newContexts.slice();
}
// ... handle additions/removals ...
}
Impact: HIGH - Prevents unnecessary re-renders of unchanged list items. Especially impactful for large lists where only one item changed.
7. Optimize Range Deletion
Location: repeater-binding-descriptor.ts - removal logic
Current Issue: Using document.createRange() and deleteContents() for every removal.
Proposal: Direct removal for better performance:
// When removing items
if (newContexts.length < renderData.renderers.length) {
// Collect nodes to remove
const nodesToRemove: Node[] = [];
for (let i = newContexts.length; i < renderData.renderers.length; i++) {
nodesToRemove.push(renderData.renderers[i].getFragmentConsumedNode());
}
// Batch removal
if (nodesToRemove.length === 1) {
nodesToRemove[0].remove();
} else if (nodesToRemove.length > 1) {
// Use range only for multiple consecutive nodes
const range = document.createRange();
range.setStartBefore(nodesToRemove[0]);
range.setEndAfter(nodesToRemove[nodesToRemove.length - 1]);
range.deleteContents();
}
renderData.renderers.length = newContexts.length;
}
Impact: LOW-MEDIUM - Slightly faster removal, avoids range creation overhead for single deletions.
8. Implement Virtual Scrolling for Large Lists
Location: New utility class, integrated with repeater-binding-descriptor.ts
Current Issue: All items are rendered even if only a few are visible (scrollable list).
Proposal: Only render visible items + buffer:
export class VirtualRepeaterBindingDescriptor extends RepeaterBindingDescriptor {
private itemHeight?: number;
private visibleCount = 20;
private bufferSize = 5;
constructor(nodeAddress: NodeAddress, bindingStatement: string, template: string, config?: {
itemHeight?: number,
visibleCount?: number,
bufferSize?: number
}) {
super(nodeAddress, bindingStatement, template);
if (config) {
this.itemHeight = config.itemHeight;
this.visibleCount = config.visibleCount || 20;
this.bufferSize = config.bufferSize || 5;
}
}
bindRenderData(node: Node, renderData: any) {
super.bindRenderData(node, renderData);
renderData.scrollContainer = this.findScrollContainer(node);
renderData.startIndex = 0;
renderData.endIndex = this.visibleCount + this.bufferSize;
if (renderData.scrollContainer) {
renderData.scrollHandler = () => this.handleScroll(renderData);
renderData.scrollContainer.addEventListener('scroll', renderData.scrollHandler);
}
}
private findScrollContainer(node: Node): HTMLElement | null {
let element = node.parentElement;
while (element) {
const overflow = window.getComputedStyle(element).overflow;
if (overflow === 'auto' || overflow === 'scroll') {
return element;
}
element = element.parentElement;
}
return null;
}
private handleScroll(renderData: any) {
if (!this.itemHeight || !renderData.scrollContainer) return;
const scrollTop = renderData.scrollContainer.scrollTop;
const newStartIndex = Math.floor(scrollTop / this.itemHeight);
const newEndIndex = newStartIndex + this.visibleCount + this.bufferSize * 2;
if (newStartIndex !== renderData.startIndex || newEndIndex !== renderData.endIndex) {
renderData.startIndex = newStartIndex;
renderData.endIndex = newEndIndex;
// Trigger re-render of visible range only
// ... implementation ...
}
}
renderDescriptor(_node: Node, context: BindingContext, renderData: any, renderingContext?: RenderingContext) {
let allContexts = context.parseRepeaterBinding(this.modelName, this.scopedName);
// Only render visible slice
const visibleContexts = allContexts.slice(
renderData.startIndex,
Math.min(renderData.endIndex, allContexts.length)
);
// Render only visible items
// ... use super logic but with visibleContexts ...
// Adjust spacing for non-rendered items
this.adjustVirtualSpacing(renderData, allContexts.length);
}
private adjustVirtualSpacing(renderData: any, totalItems: number) {
if (!this.itemHeight) return;
// Add spacers before and after visible items
const topSpacing = renderData.startIndex * this.itemHeight;
const bottomSpacing = (totalItems - renderData.endIndex) * this.itemHeight;
// ... create/update spacer elements ...
}
}
Usage:
<div repeat.for="item of largeList; virtual: true; itemHeight: 50">
${item.name}
</div>
Impact: VERY HIGH - For lists with >100 items, this can be 10-100x faster. Only renders visible items regardless of total list size.
Summary Table
| # | Optimization | Location | Impact | Complexity |
|---|---|---|---|---|
| 1 | Item Keying System | repeater-binding-descriptor.ts | HIGH | Medium |
| 2 | SubContext Initialization | sub-context-binding-descriptor.ts | MEDIUM | Low |
| 3 | Renderer Pooling | repeater-binding-descriptor.ts | MEDIUM-HIGH | Medium |
| 4 | Template Caching | template-compilation.ts | MEDIUM | Low |
| 5 | Batch DOM Operations | repeater-binding-descriptor.ts | MEDIUM-HIGH | Low |
| 6 | Partial Update Detection | repeater-binding-descriptor.ts | HIGH | Medium |
| 7 | Optimize Range Deletion | repeater-binding-descriptor.ts | LOW-MEDIUM | Low |
| 8 | Virtual Scrolling | NEW: virtual-repeater-binding-descriptor.ts | VERY HIGH | High |
Recommended Implementation Order:
- Start with #6 (Partial Update Detection) - High impact, medium complexity
- Then #1 (Item Keying) - Highest impact for reordering scenarios
- Then #4 (Template Caching) - Easy win
- Then #3 (Renderer Pooling) - Good ROI
- Finally #8 (Virtual Scrolling) - For large list scenarios
These optimizations are all specifically tailored to your repeater architecture and will significantly reduce the cost of adding/updating items in lists.