Version: 1.0.0 Techniques for optimizing render performance in Eleva applications.
Eleva gives developers direct control over the DOM through its Signal-based reactivity and Renderer. Unlike frameworks that abstract DOM updates behind a virtual DOM, Eleva’s architecture means you control exactly when and how the DOM updates by controlling signal changes.
Eleva’s automatic render batching (queueMicrotask) handles most cases efficiently—multiple signal updates in the same synchronous block trigger only one re-render. Manual optimization is needed when:
| Scenario | Problem | Solution |
|---|---|---|
| High-frequency events (mousemove, scroll) | Too many signal updates | Throttle/debounce |
| Noisy data streams (WebSocket, sensors) | Minor changes trigger renders | Conditional updates |
| Complex component with mixed update rates | Entire template re-renders | Template partitioning or component isolation |
| Animation or real-time visualization | Need direct DOM manipulation | Direct DOM access |
Since Eleva re-renders when signals change, the most direct optimization is preventing unnecessary signal updates:
Conditional Updates - Only update when changes are meaningful:
setup: ({ signal }) => {
const data = signal(0);
const updateIfSignificant = (value) => {
// Only trigger re-render if change exceeds threshold
if (Math.abs(value - data.value) > 10) {
data.value = value;
}
};
return { data, updateIfSignificant };
}
Debounced Updates - Delay updates until activity stops (ideal for input fields):
setup: ({ signal }) => {
const searchQuery = signal("");
let debounceTimer = null;
const updateDebounced = (value) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
searchQuery.value = value; // Only updates after 300ms of no input
}, 300);
};
return {
searchQuery,
updateDebounced,
onUnmount: () => clearTimeout(debounceTimer)
};
}
Throttled Updates - Limit update frequency (ideal for scroll/mousemove):
setup: ({ signal }) => {
const position = signal({ x: 0, y: 0 });
let lastUpdate = 0;
const updateThrottled = (newPos) => {
const now = Date.now();
if (now - lastUpdate >= 16) { // Cap at ~60fps
position.value = newPos;
lastUpdate = now;
}
};
return { position, updateThrottled };
}
For complex components where only part of the UI needs frequent updates, partition your template using conditional rendering to minimize DOM diffing work:
app.component("StockDashboard", {
setup: ({ signal }) => {
const stocks = signal([]); // Changes frequently
const selectedStock = signal(null); // Changes frequently
const userSettings = signal({}); // Rarely changes
const staticContent = { // Never changes - not a signal
title: "Stock Dashboard",
footer: "Data delayed 15 minutes"
};
return { stocks, selectedStock, userSettings, staticContent };
},
template: (ctx) => `
<div class="dashboard">
<!-- Static section - no signals, never re-diffed meaningfully -->
<header>${ctx.staticContent.title}</header>
<!-- Dynamic section - minimal, focused updates -->
<div class="stock-list">
${ctx.stocks.value.map(stock => `
<div key="${stock.symbol}"
class="${ctx.selectedStock.value === stock.symbol ? 'selected' : ''}"
@click="() => selectedStock.value = '${stock.symbol}'">
<span class="symbol">${stock.symbol}</span>
<span class="price">${stock.price}</span>
</div>
`).join("")}
</div>
<footer>${ctx.staticContent.footer}</footer>
</div>
`
});
Key technique: Use plain objects (not signals) for truly static content. Eleva’s renderer will still diff them, but they won’t trigger re-renders on their own.
Extract frequently-updating sections into child components. Each component has its own render cycle, so updates in a child don’t re-render the parent:
// Parent - renders only when its own signals change
app.component("Dashboard", {
setup: ({ signal }) => ({
userName: signal("John") // Rarely changes
}),
template: (ctx) => `
<div class="dashboard">
<header>Welcome, ${ctx.userName.value}</header>
<div id="live-ticker"></div> <!-- Child handles its own updates -->
<div id="chart"></div> <!-- Another isolated child -->
<footer>Static content here</footer>
</div>
`,
children: {
"#live-ticker": "LiveTicker",
"#chart": "RealTimeChart"
}
});
// Child - re-renders at 60fps without affecting parent
app.component("LiveTicker", {
setup: ({ signal }) => {
const prices = signal([]);
let intervalId = null;
return {
prices,
onMount: () => {
intervalId = setInterval(() => {
// High-frequency updates isolated to this component
prices.value = fetchLatestPrices();
}, 16);
},
onUnmount: () => clearInterval(intervalId)
};
},
template: (ctx) => `
<div class="ticker">
${ctx.prices.value.map(p => `<span key="${p.id}">${p.value}</span>`).join("")}
</div>
`
});
For maximum performance in animation-heavy or real-time visualization sections, bypass Eleva’s reactive system entirely and manipulate the DOM directly using the container reference in lifecycle hooks:
app.component("RealTimeChart", {
setup: ({ signal }) => {
const data = signal([]);
let canvas = null;
let ctx = null;
let animationId = null;
// Direct DOM manipulation for performance-critical rendering
const drawFrame = () => {
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw directly - no signals, no re-renders
const points = data.value;
ctx.beginPath();
points.forEach((point, i) => {
const x = (i / points.length) * canvas.width;
const y = canvas.height - (point / 100) * canvas.height;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
animationId = requestAnimationFrame(drawFrame);
};
// Update data without triggering Eleva re-render
const pushDataPoint = (value) => {
const current = data.value;
current.push(value);
if (current.length > 100) current.shift();
// Note: mutating in place intentionally to avoid re-render
// The canvas drawFrame reads data.value directly
};
return {
data,
pushDataPoint,
onMount: ({ container }) => {
// Get direct DOM reference
canvas = container.querySelector("canvas");
ctx = canvas.getContext("2d");
drawFrame();
},
onUnmount: () => {
cancelAnimationFrame(animationId);
}
};
},
// Template renders once, canvas updates happen via direct DOM access
template: () => `
<div class="chart-container">
<canvas width="800" height="400"></canvas>
</div>
`
});
When to use direct DOM access:
Combine reactive UI with direct DOM updates for the best of both worlds:
app.component("PerformanceMonitor", {
setup: ({ signal }) => {
// Reactive state for UI controls (normal Eleva reactivity)
const isRunning = signal(false);
const displayMode = signal("chart");
// Non-reactive state for high-frequency data
let metricsBuffer = [];
let chartElement = null;
const updateChart = () => {
if (!chartElement) return;
// Direct DOM update - bypasses Eleva entirely
chartElement.style.setProperty("--value", metricsBuffer[metricsBuffer.length - 1]);
};
const addMetric = (value) => {
metricsBuffer.push(value);
if (metricsBuffer.length > 1000) metricsBuffer.shift();
updateChart(); // Direct DOM, no re-render
};
return {
isRunning,
displayMode,
addMetric,
toggleRunning: () => { isRunning.value = !isRunning.value; },
onMount: ({ container }) => {
chartElement = container.querySelector(".chart-bar");
}
};
},
template: (ctx) => `
<div class="monitor">
<!-- Reactive UI - re-renders when signals change -->
<div class="controls">
<button @click="toggleRunning">
${ctx.isRunning.value ? "Stop" : "Start"}
</button>
<select @change="(e) => displayMode.value = e.target.value">
<option value="chart">Chart</option>
<option value="table">Table</option>
</select>
</div>
<!-- Performance-critical section - updated via direct DOM access -->
<div class="chart-bar" style="--value: 0;"></div>
</div>
`,
style: `
.chart-bar {
width: calc(var(--value) * 1%);
height: 20px;
background: linear-gradient(90deg, green, red);
transition: width 16ms linear;
}
`
});
| Update Frequency | Approach | Example |
|---|---|---|
| User-triggered (clicks, form submits) | Normal signals | Form validation, toggles |
| Moderate (every few seconds) | Normal signals | API polling, notifications |
| High (multiple per second) | Throttle/debounce signals | Search input, scroll position |
| Very high (60+ fps) | Direct DOM access | Canvas, animations, real-time charts |
| Mixed in same component | Hybrid approach | Dashboard with controls + live data |
Key insight: Eleva’s direct DOM control means you choose the optimization strategy. Gate signal updates for reactive optimization, or bypass signals entirely for maximum performance.
For large lists (1,000+ items), Eleva’s single-template approach with keyed reconciliation is the most efficient pattern:
app.component("data-table", {
setup: ({ signal }) => ({
rows: signal([])
}),
template: (ctx) => `
<table>
<tbody>
${ctx.rows.value.map(row => `
<tr key="${row.id}">
<td>${row.id}</td>
<td>${row.label}</td>
</tr>
`).join("")}
</tbody>
</table>
`
});
Why this works:
key attribute enables efficient DOM diffingAvoid component splitting for large lists:
| Pattern | 10K Rows Memory | Recommendation |
|---|---|---|
| Single template with keys | ~28.9 MB | Recommended |
| Separate component per row | ~125 MB | Avoid for large lists |
Component splitting creates per-instance overhead (signals, context, lifecycle) that far exceeds any benefit for simple list items.
For very large datasets (10,000+ rows), virtual scrolling renders only visible rows.
Verified Benchmark Results (10,000 rows):
| Metric | Standard | Virtual Scrolling | Improvement |
|---|---|---|---|
| Memory | ~29 MB | ~5 MB | 5.5x less |
| Create 10K rows | ~250ms | ~21ms | 12x faster |
| Update every 10th row | ~86ms | ~9ms | 9.5x faster |
| DOM rows rendered | 10,000 | ~17 | 588x fewer |
// Configuration
const ROW_HEIGHT = 37;
const VIEWPORT_HEIGHT = 400;
const VISIBLE_COUNT = Math.ceil(VIEWPORT_HEIGHT / ROW_HEIGHT) + 6;
app.component("virtual-table", {
setup: ({ signal }) => {
const rows = signal([]);
const scrollTop = signal(0);
const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop;
};
return { rows, scrollTop, handleScroll };
},
template: (ctx) => {
// Calculate visible slice
const allRows = ctx.rows.value;
const scroll = ctx.scrollTop.value;
const startIndex = Math.max(0, Math.floor(scroll / ROW_HEIGHT) - 3);
const endIndex = Math.min(allRows.length, startIndex + VISIBLE_COUNT);
const items = allRows.slice(startIndex, endIndex);
const offset = startIndex * ROW_HEIGHT;
const totalHeight = allRows.length * ROW_HEIGHT;
return `
<div class="virtual-viewport"
style="height: ${VIEWPORT_HEIGHT}px; overflow-y: auto;"
@scroll="handleScroll">
<div style="height: ${totalHeight}px; position: relative;">
<table style="position: absolute; top: ${offset}px; width: 100%;">
<tbody>
${items.map(row => `
<tr key="${row.id}" style="height: ${ROW_HEIGHT}px;">
<td>${row.id}</td>
<td>${row.label}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
</div>
`;
}
});
When to use:
| Dataset Size | Recommended Approach |
|---|---|
| < 1,000 rows | Single template with keys |
| 1,000 - 10,000 rows | Single template with keys (Eleva handles this efficiently) |
| 10,000+ rows | Virtual scrolling |
Tip: See the Lists - Virtual Scrolling guide for a complete implementation with search, selection, and a reusable component.
| ← Signals & Templates | Back to Overview |