GlyphUI includes several features to help you build high-performance applications. Understanding how the framework optimizes rendering will help you write efficient components.
Virtual DOM Diffing
GlyphUI uses a sophisticated diffing algorithm to minimize DOM operations. The reconciliation process compares the previous and new virtual DOM trees to determine the minimal set of changes needed.
How Diffing Works
From patch-dom.js:19, the patchDOM function implements several optimizations:
export function patchDOM(oldVdom, newVdom, parentEl, index) {
// Fast path for identical references - nothing to change
if (oldVdom === newVdom) {
return newVdom;
}
// If node types differ, replace the old node
if (oldVdom.type !== newVdom.type) {
replaceNode(oldVdom, newVdom, parentEl, index);
return newVdom;
}
// For text nodes, do a simple value check
if (newVdom.type === DOM_TYPES.TEXT && oldVdom.value === newVdom.value) {
newVdom.el = oldVdom.el;
return newVdom;
}
// Update attributes only if props changed
if (!isShallowEqual(oldVdom.props, newVdom.props)) {
updateAttributes(el, oldVdom.props, newVdom.props);
updateEventListeners(el, oldVdom, newVdom);
}
}
Optimization Techniques
- Reference equality check: If
oldVdom === newVdom, skip all updates
- Shallow props comparison: Only update DOM when props actually change
- Type-based optimization: Different node types trigger full replacement
- Selective updates: Only modify changed attributes and event listeners
The Key Prop
Using the key prop is crucial for optimizing list rendering. Keys help GlyphUI identify which items have changed, been added, or been removed.
Without Keys (Slower)
class TodoList extends Component {
render(props, state) {
return h('ul', {},
state.todos.map(todo =>
h('li', {}, [todo.text]) // No key - inefficient
)
);
}
}
Without keys, GlyphUI uses index-based reconciliation, which can lead to:
- Unnecessary DOM updates
- Loss of component state
- Broken event listeners
With Keys (Optimized)
class TodoList extends Component {
render(props, state) {
return h('ul', {},
state.todos.map(todo =>
h('li', { key: todo.id }, [todo.text]) // With key - efficient
)
);
}
}
With keys, GlyphUI uses key-based reconciliation (patch-dom.js:257):
function patchKeyedChildren(oldChildren, newChildren, parentEl) {
const oldKeyMap = new Map();
oldChildren.forEach((child, i) => {
const key = getNodeKey(child);
if (key !== undefined) {
oldKeyMap.set(key, { vdom: child, index: i });
}
});
// Efficiently reorder and update nodes based on keys
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i];
const newKey = getNodeKey(newChild);
const oldEntry = newKey !== undefined ? oldKeyMap.get(newKey) : undefined;
if (oldEntry) {
// Reuse existing node
patchDOM(oldEntry.vdom, newChild, parentEl, i);
} else {
// Create new node
mountDOM(newChild, parentEl, i);
}
}
}
Key Best Practices
Always use stable, unique identifiers as keys. Avoid using array indices as keys when the list can be reordered.
// Good: Stable unique IDs
state.items.map(item =>
h('div', { key: item.id }, [item.name])
)
// Bad: Array indices (order can change)
state.items.map((item, index) =>
h('div', { key: index }, [item.name])
)
// Bad: Non-unique keys
state.items.map(item =>
h('div', { key: item.category }, [item.name])
)
Memoization with useMemo
The useMemo hook caches expensive computations and only recalculates when dependencies change.
Basic Usage
import { useState, useMemo } from 'glyphui';
function ExpensiveComponent({ numbers }) {
const [filter, setFilter] = useState('');
// Only recalculate when numbers or filter changes
const filteredNumbers = useMemo(() => {
console.log('Filtering numbers...');
return numbers.filter(n => n.toString().includes(filter));
}, [numbers, filter]);
return h('div', {}, [
h('input', {
value: filter,
on: { input: (e) => setFilter(e.target.value) }
}),
h('ul', {},
filteredNumbers.map(n =>
h('li', { key: n }, [n.toString()])
)
)
]);
}
Prime Number Example
From examples/performance/prime-calculator.js:8, here’s a real-world example:
const findLargestPrime = (limit) => {
console.log(`Calculating largest prime below ${limit}...`);
let largestPrime = 2;
for (let i = 3; i < limit; i += 2) {
let isPrime = true;
for (let j = 3; j * j <= i; j += 2) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
largestPrime = i;
}
}
return largestPrime;
};
class PrimeCalculator extends Component {
constructor() {
super({}, {
initialState: {
numberLimit: 10000,
unrelatedState: 0,
largestPrime: 2,
lastCalculatedLimit: 0
}
});
}
// Only recalculate when numberLimit changes
calculateLargestPrime(numberLimit) {
if (numberLimit !== this.state.lastCalculatedLimit) {
return findLargestPrime(numberLimit);
}
return this.state.largestPrime;
}
incrementCounter() {
// This update doesn't trigger prime recalculation
this.setState({ unrelatedState: this.state.unrelatedState + 1 });
}
}
Don’t overuse memoization. Simple calculations are often faster than the overhead of memoization. Profile before optimizing.
Callback Memoization with useCallback
The useCallback hook memoizes function references to prevent unnecessary re-renders of child components.
The Problem
function ParentComponent() {
const [count, setCount] = useState(0);
// New function created on every render
const handleClick = () => {
console.log('Clicked');
};
// Child re-renders even if props haven't changed
return createComponent(ChildButton, { onClick: handleClick });
}
The Solution
import { useState, useCallback } from 'glyphui';
function ParentComponent() {
const [count, setCount] = useState(0);
// Function reference stays the same between renders
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Empty deps = never recreate
// Child only re-renders when handleClick changes
return createComponent(ChildButton, { onClick: handleClick });
}
Class Component Alternative
In class components, bind methods in the constructor for similar benefits:
class ParentComponent extends Component {
constructor() {
super({}, {
initialState: { count: 0 }
});
// Bind once in constructor
this.handleThemeToggle = this.handleThemeToggle.bind(this);
this.handleCounterReset = this.handleCounterReset.bind(this);
}
handleThemeToggle() {
// Method reference doesn't change between renders
this.setState({ theme: this.state.theme === 'light' ? 'dark' : 'light' });
}
handleCounterReset() {
this.setState({ count: 0 });
}
render(props, state) {
return h('div', {}, [
// These callbacks maintain referential equality
createComponent(Button, {
onClick: this.handleThemeToggle
}, ['Toggle Theme']),
createComponent(Button, {
onClick: this.handleCounterReset
}, ['Reset Counter'])
]);
}
}
Component Optimization Patterns
Avoid Creating New Objects in Render
// Bad: New object created on every render
render() {
return h('div', {
style: { padding: '20px', margin: '10px' } // New object
}, ['Content']);
}
// Good: Reuse object references
const containerStyle = { padding: '20px', margin: '10px' };
render() {
return h('div', {
style: containerStyle // Same reference
}, ['Content']);
}
// Bad: Recreated on every render
render(props, state) {
return h('div', {}, [
h('header', {}, [
h('h1', {}, ['My App']),
h('nav', {}, [/* complex navigation */])
]),
h('main', {}, [state.content])
]);
}
// Good: Extract static header
class Header extends Component {
render() {
return h('header', {}, [
h('h1', {}, ['My App']),
h('nav', {}, [/* complex navigation */])
]);
}
}
render(props, state) {
return h('div', {}, [
createComponent(Header),
h('main', {}, [state.content])
]);
}
Shallow Props Comparison
GlyphUI automatically performs shallow comparison of props to skip unnecessary updates:
// From patch-dom.js:68
if (isShallowEqual(oldVdom.props, newVdom.props)) {
// Props are the same, reuse the old element
newVdom.el = oldVdom.el;
if (newVdom.type === COMPONENT_TYPE) {
newVdom.instance = oldVdom.instance;
}
return newVdom;
}
This means components won’t re-render if their props haven’t changed.
Batching State Updates
Minimize re-renders by batching multiple state updates:
// Bad: Multiple re-renders
handleUpdate() {
this.setState({ loading: true });
this.setState({ error: null });
this.setState({ data: newData });
}
// Good: Single re-render
handleUpdate() {
this.setState({
loading: true,
error: null,
data: newData
});
}
Console Logging
Add strategic console logs to identify unnecessary renders:
class MyComponent extends Component {
render(props, state) {
console.log('MyComponent rendered', { props, state });
return h('div', {}, ['Content']);
}
}
Measure expensive operations:
function expensiveCalculation(data) {
const start = performance.now();
const result = /* ... */;
const end = performance.now();
console.log(`Calculation took ${end - start}ms`);
return result;
}
Best Practices Summary
- Use keys in lists: Always provide unique, stable keys for list items
- Memoize expensive computations: Use
useMemo for costly calculations
- Memoize callbacks: Use
useCallback or bind methods in constructor
- Avoid inline objects: Extract static objects to prevent unnecessary re-renders
- Batch state updates: Update multiple state values in a single call
- Extract static components: Move unchanging UI to separate components
- Profile before optimizing: Measure actual performance impact before adding complexity
Premature optimization is the root of all evil. Profile your application first, identify bottlenecks, then apply targeted optimizations.