Performance
Rendering Performance
DOM optimization, layout thrashing, animation performance, and React/Vue optimization
Rendering Performance
Rendering performance affects how smooth and responsive your application feels. Understanding the browser rendering pipeline is key to optimization.
Browser Rendering Pipeline
Rendering Pipeline
├── JavaScript (scripting)
├── Style (CSS calculation)
├── Layout (geometry calculation)
├── Paint (fill pixels)
└── Composite (layer composition)
Optimization Goals:
- Minimize JavaScript execution time
- Reduce style recalculations
- Avoid forced synchronous layouts
- Promote animations to compositor
- Keep frame time under 16ms (60fps)Layout Thrashing
Layout thrashing occurs when JavaScript reads and writes layout properties repeatedly.
Problem
// Bad - causes layout thrashing
function resizeAllDivs() {
const divs = document.querySelectorAll('.box');
divs.forEach(div => {
// Read - forces layout
const width = div.offsetWidth;
// Write - invalidates layout
div.style.width = (width * 2) + 'px';
// Next read forces another layout...
});
}Solution
// Good - batch reads then writes
function resizeAllDivs() {
const divs = document.querySelectorAll('.box');
// Batch reads
const widths = Array.from(divs).map(div => div.offsetWidth);
// Batch writes
divs.forEach((div, i) => {
div.style.width = (widths[i] * 2) + 'px';
});
}
// Better - use requestAnimationFrame
function resizeAllDivs() {
const divs = document.querySelectorAll('.box');
const widths = Array.from(divs).map(div => div.offsetWidth);
requestAnimationFrame(() => {
divs.forEach((div, i) => {
div.style.width = (widths[i] * 2) + 'px';
});
});
}Properties That Trigger Layout
// These properties trigger layout when read:
element.offsetTop, offsetLeft, offsetWidth, offsetHeight
element.offsetParent
element.clientTop, clientLeft, clientWidth, clientHeight
element.scrollTop, scrollLeft, scrollWidth, scrollHeight
element.getComputedStyle()
element.getBoundingClientRect()
element.scrollTo()
window.innerWidth, innerHeight
window.scrollX, scrollYAnimation Performance
CSS Animations (Preferred)
/* Good - compositor-only properties */
.animate-good {
transform: translateX(100px);
opacity: 0.5;
/* These don't trigger layout or paint */
}
/* Bad - triggers layout */
.animate-bad {
left: 100px;
width: 200px;
height: 200px;
}
/* Promote to compositor layer */
.promoted-layer {
will-change: transform;
/* or */
transform: translateZ(0);
}JavaScript Animations
// Good - use requestAnimationFrame
function animate(element, duration) {
const start = performance.now();
const initialX = 0;
const targetX = 100;
function frame(time) {
const progress = Math.min((time - start) / duration, 1);
const eased = easeOutCubic(progress);
const x = initialX + (targetX - initialX) * eased;
element.style.transform = `translateX(${x}px)`;
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}Web Animations API
element.animate(
[
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0 }
],
{
duration: 300,
easing: 'ease-out',
fill: 'forwards'
}
);Virtual Lists
For long lists, only render visible items.
// Using @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}React Optimization
Preventing Unnecessary Renders
// memo for expensive components
const ExpensiveList = memo(function ExpensiveList({ items }) {
return items.map(item => <ExpensiveItem key={item.id} item={item} />);
});
// Custom comparison
const UserCard = memo(
function UserCard({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);useMemo and useCallback
function SearchResults({ query, items }) {
// Memoize expensive computation
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [items, query]);
// Memoize callback to prevent child re-renders
const handleSelect = useCallback((id: number) => {
console.log('Selected:', id);
}, []);
return (
<ul>
{filteredItems.map(item => (
<ListItem key={item.id} item={item} onSelect={handleSelect} />
))}
</ul>
);
}State Colocation
// Bad - state too high, causes unnecessary renders
function App() {
const [searchQuery, setSearchQuery] = useState('');
return (
<div>
<Header /> {/* Re-renders on every keystroke */}
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<Results query={searchQuery} />
<Footer /> {/* Re-renders on every keystroke */}
</div>
);
}
// Good - state colocated with consumers
function App() {
return (
<div>
<Header />
<SearchSection /> {/* Contains its own state */}
<Footer />
</div>
);
}
function SearchSection() {
const [searchQuery, setSearchQuery] = useState('');
return (
<>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<Results query={searchQuery} />
</>
);
}Vue Optimization
v-once and v-memo
<template>
<!-- Never re-renders -->
<div v-once>{{ staticContent }}</div>
<!-- Only re-renders when dependencies change -->
<div v-memo="[item.id, item.selected]">
{{ item.name }}
</div>
<!-- Efficient list rendering -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
{{ item.name }}
</div>
</template>Computed Property Caching
<script setup>
// Cached - recalculates only when dependencies change
const filteredList = computed(() => {
return items.value.filter(item => item.active);
});
// Not cached - recalculates on every render
const getFilteredList = () => {
return items.value.filter(item => item.active);
};
</script>shallowRef for Large Objects
<script setup>
import { shallowRef, triggerRef } from 'vue';
// For large objects that don't need deep reactivity
const largeObject = shallowRef({ nested: { data: [] } });
function updateData() {
largeObject.value.nested.data.push(newItem);
triggerRef(largeObject); // Manually trigger update
}
</script>Debouncing and Throttling
// Debounce - wait until user stops typing
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Throttle - limit execution frequency
function useThrottle<T>(value: T, interval: number): T {
const [throttledValue, setThrottledValue] = useState(value);
const lastExecuted = useRef(Date.now());
useEffect(() => {
const now = Date.now();
const elapsed = now - lastExecuted.current;
if (elapsed >= interval) {
setThrottledValue(value);
lastExecuted.current = now;
} else {
const timer = setTimeout(() => {
setThrottledValue(value);
lastExecuted.current = Date.now();
}, interval - elapsed);
return () => clearTimeout(timer);
}
}, [value, interval]);
return throttledValue;
}Best Practices
Rendering Performance Guidelines
- Avoid layout thrashing - batch DOM reads and writes
- Use transform and opacity for animations
- Implement virtual scrolling for long lists
- Memoize expensive computations and callbacks
- Keep component state as low as possible
- Use debouncing/throttling for frequent updates
- Profile with React DevTools / Vue DevTools
- Target 60fps (16ms per frame)