Frontend Algorithmic Thinking
Performance Big-O Analysis
The Cost of a Re-Render
React's magic is its ability to keep the UI in sync with your application's state. When state changes, React figures out the smallest possible change to make to the actual DOM. This process is called reconciliation and involves a "diffing" algorithm that compares a new tree of elements with the previous one.
Conceptually, comparing two trees is an problem, where is the number of elements. That's incredibly slow. Thankfully, React uses a clever heuristic. By assuming that two elements of different types will produce different trees, and by using the key prop to track elements across renders, it simplifies the problem to something much closer to .
This is a massive performance win. However, the work done inside your components during the render phase is entirely your responsibility. React's efficient diffing won't save you if your own code is slow. This is where we apply Big-O analysis to our own components.
Auditing the Render Cycle
The most common performance killer in React apps is doing too much work during the render cycle. Every time a component's state or props change, its body is executed again. If you have expensive operations there, they'll run on every single render.
A classic mistake is filtering or transforming a large list directly inside the component body without memoization.
Imagine a dashboard with thousands of transactions. If a user types into a search box, the component re-renders on every keystroke. Let's see what that looks like.
function TransactionList({ transactions, searchTerm }) {
// This runs on EVERY render!
const visibleTransactions = transactions.filter(t =>
t.description.includes(searchTerm)
);
return (
<ul>
{visibleTransactions.map(t => <li key={t.id}>{t.description}</li>)}
</ul>
);
}
Here, transactions.filter() is an operation, where is the total number of transactions. If the transactions array has 10,000 items, you're iterating over 10,000 items on every keystroke. This can make the UI feel sluggish.
As we've covered, the solution is useMemo. By wrapping the expensive calculation, you ensure it only re-runs when its dependencies (transactions or searchTerm) actually change.
const visibleTransactions = useMemo(() => {
return transactions.filter(t =>
t.description.includes(searchTerm)
);
}, [transactions, searchTerm]);
The Cost of Immutability
Updating state in React requires immutability. You can't just push an item into an array or change an object's property directly. Instead, you create a new copy with the updated value. The way you create this copy has performance implications.
The object spread operator (...) is a common tool for this. When updating an array, you might do this:
const updatedItems = [...oldItems, newItem]; // Creates a new array
This operation has a cost. It needs to copy every reference from oldItems into the new array. This makes it an operation. For most UI lists, this is perfectly fine. But if you're working with tens of thousands of items, this copy operation, performed on every small change, can add up.
This is why we previously discussed using Hash Maps (or JavaScript objects) for large collections. Updating a single item in a map is a near-instant operation.
| Operation | Array (.slice, ...spread) | Map (.set) |
|---|---|---|
| Add/Update Item | to copy | on average |
| Find Item by ID | to find | on average |
When you use a map for your state, updating a single item looks like this:
setState(prevItems => ({
...prevItems, // O(n) copy of the map's keys
[itemId]: updatedItem, // O(1) update for the specific item
}));
The spread operator still copies the top-level keys of the object, which is an operation where is the number of items. However, the values themselves (the items) are not deep-copied, just their references. While better than cloning everything, for extremely high-performance scenarios with huge, frequently updated maps, specialized libraries like can optimize this further by using structural sharing to avoid unnecessary copying.
Using immutable data structures in a React application can improve performance by allowing the application to take advantage of reference equality checks.
Ready to test your knowledge on performance bottlenecks?
Conceptually, comparing two UI trees is an incredibly complex problem. What is the Big-O notation for this operation before React applies its optimization heuristics?
What is the primary role of the key prop when rendering a list of elements in React?
Understanding these complexities helps you write applications that not only work but feel fast and responsive, even as the data they handle grows.
