Big-O without the math: what your code really costs

Your code runs perfectly on ten items and stutters on ten thousand - even though not a single line changed. Big-O doesn't tell you "how fast," only "how the cost grows as the amount of data grows." How to spot the shape of that growth in everyday frontend code - and how to turn a loop inside a loop into something that doesn't slow down on large data.
Your list works perfectly. You have ten items, you filter them, you render them - everything’s snappy. Then the real data arrives, ten thousand items, and the page stutters for a second on every click. The code didn’t change. Only the amount of data flowing through it did.
That’s exactly the question Big-O answers: not “how fast is this code,” but how its cost grows as the amount of data grows. Because “fast” depends on the machine, the browser, and how busy they are right now. How the cost scales with the amount of data, though, is a property of the code itself - and you can spot it before that real data ever shows up.
#We count operations, not seconds
Let’s agree on one symbol: n is the amount of data - the number of array items, rows, nodes. Big-O asks: how many operations does the code do for n items, as n grows. It’s not about the exact count down to the last one, but about the shape: does the cost grow as fast as the data, slower, or much faster?
The easiest way to see it is on a few common cases, each on the kind of code you write every day.
#O(1) - constant cost
Accessing an array element by index (arr[5]) is one operation, no matter whether the array has ten items or ten million. A lookup in a Map or Set (that is, finding a value by its key - map.get(key), set.has(x)) is on average O(1). Underneath it’s hashing: the key is turned into a number that points straight to a spot in memory, so instead of scanning everything one by one, you go straight to the target. The cost doesn’t grow with n. It’s the cheapest thing you have.
#O(n) - linear cost
One pass over the list: .map(), .filter(), .find(), a plain for loop. For n items you do n operations. Double the list - double the cost. That’s a natural, acceptable price: you can’t do most things in a UI any cheaper, because you have to touch each item at least once. O(n) isn’t a problem. The problem starts when O(n) gets nested inside another O(n).
#O(n²) - quadratic cost (where the pain begins)
A loop inside a loop. The most common hidden culprit in frontend code looks innocent - it’s a .find() inside a .map():
// for each user we look up their order
const rows = users.map((user) => ({
...user,
order: orders.find((o) => o.userId === user.id), // searches from scratch
}));For each of n users, find walks the whole list of orders. n times n operations is n². At a hundred items that’s ten thousand operations - unnoticeable. At ten thousand it’s a hundred million - and that’s where the stutter comes from. The code is correct, readable, and works on the demo. It only stalls on real data.
#The fix: turn O(n²) into O(n)
The trick is almost always the same: build an index once, then reach into it instead of searching the whole list every time.
// build the index once: userId -> order (one pass, O(n))
const orderByUser = new Map(orders.map((o) => [o.userId, o]));
// now every lookup is instant (on average O(1))
const rows = users.map((user) => ({
...user,
order: orderByUser.get(user.id),
}));Building the map is one pass over the orders - O(n). After that, every check is instant: you no longer scan the whole list, you go straight to the right entry. Together that’s O(n) instead of O(n²) - the same result, far lower cost. This is the most common real Big-O win in frontend work: you spot a loop inside a loop and swap the inner search for a map. And the page no longer slows down, even on real data.
#O(log n) - logarithmic cost
There’s a pattern even cheaper than linear. Imagine looking up a word in a paper dictionary. You don’t read it from the start - you open it in the middle, see which way to go, and throw away the other half. Then the middle again. At each step you’re left with half the data.
That’s O(log n): doubling the data adds one step, it doesn’t double the cost. A thousand items is about ten steps, a million about twenty. That’s why a well-indexed search feels instant, even on huge sets. (A Map lookup is even better, on average O(1), but a logarithm still grows remarkably slowly.)
#O(n log n) - sorting
Array.prototype.sort is O(n log n) - slower than a single pass, but much faster than a loop inside a loop. Sorting a list once is usually fine. Sorting it inside a loop, on every change, is not - because then O(n log n) gets multiplied by another n and you’re back to quadratic cost.
Drag n and see how many operations each complexity needs. At a small n the results are similar; at a large n, O(n²) blows up while O(1) still shows 1.
- O(1)1
- O(log n)4
- O(n)10
- O(n²)100
#Why the constant disappears
You’re probably thinking: but one pass does two operations per item (that’s 2n), and another does one operation per item plus a couple at startup (that’s n + 5). In Big-O all of that is O(n). The notation deliberately drops constants and lower-order terms, because what it cares about is the shape of the growth at a large n, not the exact number. n² will eventually overtake 1000n anyway - all it takes is for n to grow. You don’t count operations one by one. You recognise which family your code belongs to.
#Time isn’t everything - there’s memory too
Big-O also measures memory use, and you count it the same way as time - except instead of operations you look at how much extra memory the code takes as n grows. A loop that sums values into a single variable takes the same amount of memory regardless of the list’s length: that’s O(1). Building a new array or map of n items takes memory that grows with n: that’s O(n).
That’s why the map we built above costs O(n) memory - it holds n entries that weren’t there before. It’s a classic trade: you give up a bit of memory to gain time. Cutting the time from O(n²) to O(n) cost you O(n) of extra memory (that map). It’s usually a great deal, but a deliberate choice, not magic: on truly huge sets, memory runs out too.
#What to take away
You don’t need the math. You need a feel for how the cost grows - and it comes down to three questions:
- Does the cost grow at the same rate as the data, or faster?
- Is there a loop inside a loop - am I searching for something (
find,includes,indexOf) inside another loop? - Will doubling the list double the cost, or quadruple it?
Once these three questions become a habit, you can see the cost in the code at ten items - long before the data grows to ten thousand and everything stalls.
Comments
Loading comments…