Big O notation: Avoiding common performance pitfalls

JavaScript

Not every piece of code needs to be perfectly optimized. Over-focusing on performances can sometimes lead to code that’s unnecessarily complex and unreadable without real benefits. However, being aware of common performance pitfalls lets us navigate and avoid them effectively.

What is performance?

When we talk about performance, we’re referring to how efficiently a program runs. Typically, we analyze it in terms of time and memory usage:

  • Analyzing the time allows us to describe how long a program takes to execute.
  • Analyzing memory lets us point out how much memory a program consumes when running.

Sometimes, we can make our application faster by letting it consume more memory and vice versa. Our goal isn’t always to make everything as fast as possible but to find the right trade-offs for a given situation.

Nowadays, we tend to focus more on optimizing time than memory consumption. In most cases, slow execution is a more significant bottleneck than high memory usage. Because of that, we focus on the time complexity in this article.

Analyzing performance

The performance of our application can depend on multiple things, and one of them is how well we write our code. However, there are also outside factors, such as the machine we run it on. Because of that, we need an objective way of analyzing the performance that’s not affected by variables like that.

Let’s start with a simple array of users.

Now, let’s write a function that returns a user with a given id.

There are a couple of ways to look at the efficiency of our function.

The best-case scenario

In the best-case scenario, the user we are looking for is in the first element of our array. Thanks to that, our function needs to enter the loop only once.

The worst-case scenario

In the worst-case scenario, the user we’re looking for is in the last element of the array. To find it, our function needs to go through the entire array.

When analyzing performance, we typically care the most about the worst-case scenario.

The Big O notation

When analyzing performance, we don’t necessarily count the exact number of operations our code needs to perform. Instead, we examine how the number of operations grows as our dataset increases. For example, if we have an array with ten elements, our function needs to check all ten elements in the worst case before finding a match. If the array size triples, the number of checks also triples since the loop goes through every element in the array.

We can use the Big O notation to describe the performance of our function in the worst-case scenario. It allows us to note how quickly the number of steps in our code grows as the dataset gets bigger. The time complexity of our function is O(n), where represents the size of our dataset.

The O(n) complexity means that in the worst-case scenario, the number of steps grows at the same rate as the input size. For example, if our array grows ten times, processing it will take roughly ten times more steps.

O(n) is called a linear time complexity where the number of steps grows at the same rate as the input size. The O(n) complexity is rather common and usually acceptable if our dataset is not huge and the function isn’t executed too frequently.

Quadratic time complexity

Let’s write a function that checks if some users share the same name.

In the above function, we go through each user in our array and check how many users share their names.

A big red flag we should notice is that our function has a loop nested inside of another loop. For example, if our array has ten elements, the following code runs a hundred times in the worst-case scenario:

If the size of our dataset is multiplied n times, the number of steps increases times. For example, if the array size increases 5 times, the number of operations is increased 25 times.

Because of the above, the time complexity of our function is O(n²).

In the above code, it’s easy to notice because we’re putting one loop directly inside another one. It might be slightly less obvious if our function calls another function that uses a loop. It’s also crucial to know that functions like , , , or perform loops under the hood.

Above, we use the function inside . Because of that, the time complexity of is still O(n²).

Thankfully, we can rewrite our function so that it has the O(n) complexity.

Technically, the complexity of our above function could be described as O(2 * n) because we’re doing two loops. However, in Big O notation, we omit constant factors such as multiplying by a number. Instead, we focus on how the number of steps increases relative to the input size rather than the exact number of operations. Therefore, we can say that the complexity of our function is O(n).

Still, that should not stop us from improving our function to use a single loop.

Preparing our data

Previously, we wrote a function that returns a user with a given id.

We established that its time complexity is O(n). It’s fine as long as our dataset is not too big and we don’t use this function very often. However, if we have a lot of elements in our array and we call the function often, its performance might not be good enough.

To change that, we can transform our array of users to an object where each key corresponds with the user ID.

Transforming our array into the above dictionary has the complexity of O(n) because we loop over each user once. This complexity is the same as the function we wrote before. However, if the array of users doesn’t change, we need to do this only once.

Now, we can access each user instantly.

Thanks to the above change, our function now has the O(1) complexity. This means the number of operations doesn’t change when the dataset grows. Even if we had ten times as many users, the function would perform the same number of operations.

The above is a good example of increasing our memory complexity to decrease the time complexity. When doing that, we had to create the variable that takes additional memory.

Summary

In this article, we’ve explained how we can analyze the performance of our code. To do that, we used the Big O notation and explored different time complexities. We also noticed that restructuring our data, such as converting an array to an object, can help us make our code faster by increasing memory usage.

Understanding how to analyze performance can help us write more scalable code. While we shouldn’t focus on optimization prematurely, understanding the basic principles can help us avoid common performance pitfalls.

Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Ulrich Mbouna
1 month ago

Really interesting