Performance Optimization
Inglorious Web is designed to be fast, but here are tips to optimize further.
Measurement
First, measure your actual performance using the browser's Performance API:
const store = createStore({ types, entities })
store.subscribe(() => {
const start = performance.now()
renderApp(store.api)
const end = performance.now()
if (end - start > 16) {
console.warn(`Slow render: ${end - start}ms`)
}
})
2
3
4
5
6
7
8
9
10
11
Use browser DevTools Performance API:
// Profile rendering
performance.mark("render-start")
// ... render code ...
performance.mark("render-end")
performance.measure("render", "render-start", "render-end")
2
3
4
5
Or open the browser's Performance tab in DevTools to see frame rates and rendering times.
Full-Tree Render Optimization
1. Break Into Multiple Entities
Instead of one massive entity:
// ❌ Bad: Monolithic
const app = {
render(entity, api) {
return html`
<header><!-- 100 lines --></header>
<sidebar><!-- 200 lines --></sidebar>
<main><!-- 300 lines --></main>
`
},
}
// ✅ Good: Composed
const app = {
render(entity, api) {
return html`
<div>
${api.render("header")} ${api.render("sidebar")} ${api.render("main")}
</div>
`
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Smaller entities re-render faster because each is simpler.
2. Use Memoization
Cache expensive computations:
import { compute } from '@inglorious/store'
const getExpensiveValue = compute(
(items) => {
// Only runs if items changes
return items
.filter(x => x.value > 0)
.map(x => processItem(x))
.reduce((sum, x) => sum + x.value, 0)
},
[() => api.getEntity('list').items]
)
render(entity, api) {
const total = getExpensiveValue()
return html`<p>Total: ${total}</p>`
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3. Avoid Creating Objects in Render
// ❌ Bad: New object every render
render(entity, api) {
const styles = { color: entity.color, fontSize: '16px' }
return html`<div style=${JSON.stringify(styles)}>Text</div>`
}
// ✅ Good: Use styleMap
import { styleMap } from 'lit-html'
render(entity, api) {
return html`
<div style=${styleMap({
color: entity.color,
fontSize: '16px',
})}>Text</div>
`
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List Performance
Use repeat() Directive
import { repeat } from 'lit-html'
render(entity, api) {
return html`
<ul>
${repeat(
entity.items,
item => item.id, // Key function
(item, index) => html`
<li>${item.name}</li>
`
)}
</ul>
`
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
This helps lit-html track which item is which during reorders.
Virtual Scrolling for Large Lists
import { list } from "@inglorious/web/list"
const types = {
itemList: {
...list,
renderItem(item, index) {
return html`<div>${item.name}</div>`
},
},
}
const entities = {
items: {
type: "itemList",
items: hugeArray,
viewportHeight: 600,
itemHeight: 50, // Fixed height for efficiency
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Only renders visible items, huge performance gain for 1000+ items.
Event Batching
Events are automatically batched into a queue for atomic updates:
// These all batch together
api.notify("#item:select", 1)
api.notify("#item:highlight", 1)
api.notify("#list:refresh")
// Result: Single re-render for all three events
2
3
4
5
6
This is optimal for performance. For manual control in game loops or animations:
const store = createStore({
types,
entities,
updateMode: "manual", // Don't update automatically
})
// Trigger events, then manually call update
function gameLoop() {
api.notify("#player:move", { x: 10 })
api.notify("#enemy:update")
// All events processed, render happens here
store.update()
requestAnimationFrame(gameLoop)
}
gameLoop()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
This gives you precise control over when re-renders happen, which is important for:
- Game loops (60 FPS sync)
- Animation loops
- Matching other update frequencies
DOM Diffing
lit-html's template diffing is already optimized. A few tips:
Cache Templates
// ❌ Creates new template every render
const myTemplate = () => html`<div>${value}</div>`
// ✅ Reuse template
const template = html`<div>${value}</div>`
render(entity, api) {
return template
}
2
3
4
5
6
7
8
Use Directives
import { when, choose } from 'lit-html'
// when: Avoids building template if condition is false
render(entity, api) {
return when(
entity.isLoading,
() => html`<p>Loading...</p>`,
() => html`<p>Done!</p>`
)
}
2
3
4
5
6
7
8
9
10
Async Operations
Minimize Blocking
// ❌ Blocks render
const data = await fetchData()
render() // Can't start until fetch completes
// ✅ Non-blocking - set state before await, notify event after
async fetchData(entity, api) {
entity.isLoading = true
try {
const data = await fetch('/api/data').then(r => r.json())
// After await, notify event instead of updating entity
api.notify("#dataLoader:fetchSuccess", data)
} catch (error) {
api.notify("#dataLoader:fetchError", error.message)
}
}
fetchSuccess(entity, data) {
entity.isLoading = false
entity.data = data
}
fetchError(entity, error) {
entity.isLoading = false
entity.error = error
}
// UI updates as loading state changes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Debounce Expensive Operations
const debounced = (type) => {
let timeout
return {
search(entity, term, api) {
clearTimeout(timeout)
entity.searchTerm = term
entity.isSearching = true
timeout = setTimeout(async () => {
try {
const results = await searchAPI(term)
entity.results = results
} finally {
entity.isSearching = false
}
}, 300)
},
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Bundle Size
All sizes below are gzipped and intended as a high-level comparison.
| Package / Bundle | Size (gzipped) |
|---|---|
| React + RTK | 74.9 KB |
| Vue + Pinia | 73.9 KB |
| React | 60.4 KB |
| Vue | 47.2 KB |
| Inglorious Web | 15.4 KB |
| Svelte | 15 KB |
| Solid | 8.5 KB |
To reduce bundle size further:
- Use tree-shaking — only import what you need
- Avoid importing unused components
- Lazy-load routes and heavy modules
- Use dynamic imports
// Good: Only import what you use
import { form } from "@inglorious/web/form"
import { router } from "@inglorious/web/router"
// Lazy load
const admin = () => import("./pages/admin")
2
3
4
5
6
When Performance Matters Most
Critical Paths
Optimize high-traffic paths:
- Initial page load — Lazy load heavy routes
- Frequent interactions — Debounce searches, use memoization
- Large lists — Use virtual scrolling
- Expensive computations — Use
compute()memoization
Profiling Tools
Use browser DevTools:
- Performance tab — Measure rendering time
- Performance Observer — Track custom metrics
- Lighthouse — Audit overall performance
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 16) {
console.warn(`Slow: ${entry.name} (${entry.duration}ms)`)
}
}
})
observer.observe({ entryTypes: ["measure"] })
2
3
4
5
6
7
8
9
Benchmarking
Compare approaches:
const benchmark = async (name, fn, iterations = 1000) => {
const start = performance.now()
for (let i = 0; i < iterations; i++) {
fn()
}
const end = performance.now()
console.log(
`${name}: ${(end - start / iterations).toFixed(3)}ms per iteration`,
)
}
benchmark("Full render", () => renderApp(api))
benchmark("Handler call", () => counter.increment(entity))
2
3
4
5
6
7
8
9
10
11
12
13
Best Practices
✅ Do:
- Measure first, optimize second
- Break large entities into smaller ones
- Use
repeat()for lists - Use virtual scrolling for 1000+ items
- Memoize expensive computations
- Batch events (automatic by default)
- Lazy load routes and heavy components
- Profile with browser DevTools
❌ Don't:
- Optimize prematurely
- Create objects in render
- Forget memoization for expensive operations
- Use full-tree renders for 10,000+ DOM nodes
- Load everything upfront
- Ignore bundle size
Next Steps
- Testing — Performance testing patterns
- Error Handling — Robust error handling
Happy optimizing! ⚡
Inglorious Web