Looking to optimize your code? You'll need to test it.
By testing different approaches, you can compare them fairly and choose the most performant option.
You don't need any special tools to do this - your browser console can compare the speed of individual functions easily!
A Reusable Benchmark Utility Function
I wrote a utility to make it easy for you to test the performance of your functions. After several iterations and improvements, this is the code:
const benchmark = ({
runCount = 10_000_000,
getArgs = () => [],
testFunctions = {},
}) => {
const test = (fn) => {
const start = performance.now();
for (let i = 0; i < runCount; i++) {
const args = getArgs();
fn(...args);
}
const end = performance.now();
const runtime = end - start;
return runtime;
};
const results = {};
for (const funcName in testFunctions) {
const fn = testFunctions[funcName];
results[funcName] = test(fn);
}
return results;
};
To use this utility, you'll need to provide the functions you want to test in the testFunctions
object.
Also, provide an optional getArgs
function to generate the arguments for you tests. You can have getArgs
return the same value for each test or random arguments. I prefer to randomize the inputs to make sure the JavaScript engine isn't somehow optimizing the code based on a static and predictable argument, because I know the arguments will not be static or predictable in production!
Testing Performance in Practice
Here's how to use it:
// functions to test
const getHighestNumber1 = (numberArray) => {
let highest = numberArray[0];
for (let i = 1; i < numberArray.length; i++) {
if (numberArray[i] > highest) {
highest = numberArray[i];
}
}
return highest;
};
const getHighestNumber2 = (numberArray) => {
let highest = numberArray[0];
for (let i = 1; i < numberArray.length; i++) {
let num = numberArray[i];
if (num > highest) {
highest = num;
}
}
return highest;
};
const getHighestNumber3 = (numberArray) => {
let highest = numberArray[0];
numberArray.forEach(num => {
if (num > highest) {
highest = num;
}
});
return highest;
};
const getHighestNumber4 = (numberArray) => {
return Math.max(...numberArray);
};
// benchmark the functions!
const results = benchmark({
runCount: 10_000_000,
getArgs: () => {
const numberArray = [];
for (let i = 0; i < 10; i++) {
numberArray.push(Math.random() * 100);
}
return [numberArray];
},
testFunctions: {
getHighestNumber1,
getHighestNumber2,
getHighestNumber3,
getHighestNumber4,
},
});
I suggest running the test several times in your browser console to be sure of the results.
After running the tests above, it is clear that getHighestNumber1
and getHighestNumber2
are roughly equal in performance, while getHighestNumber3
is just a bit slower. Looks like that Array.prototype.forEach
call comes at a cost!
Finally, we see that getHighestNumber4
lags behind significantly, taking 2x to 3x as long to execute as the other options. Even though Math.max
is built into JavaScript, it has worse performance than writing your own for
loop and if
statement!
A Word of Caution
Although the example results above make Math.max
look pretty bad, I wouldn't stop using Math.max
. Speaking from experience, it's usually more important to be able to read your code easily than it is to squeeze every last millisecond of performance out of it.
A for
loop and if
statement might be a tad faster, but Math.max
is far more concise and make your intent immediately clear. Before you try to optimize all of your code, remember that network calls (to APIs, databases, etc.) take far more time than a local function call. Be sure you are optimizing where it matters, and only when it matters!
Other Considerations
In my utility function, I called getArgs
within the innermost for
loop, which also calls the function to be tested. The advantage of this is that each time the test function is called, it will have completely new arguments. This can help prevent the JavaScript engine from optimizing the code in a way that doesn't represent a production environment.
However, one disadvantage of calling getArgs
at that location is that it makes the test slower. The call time of getArgs
ends up as part of the test... for each iteration. Since getArgs
is the same for each of your test functions, and since it is called the same number of times, this is still a fair approach. But it is slower.
Below, I labelled two alternative locations for calling getArgs
.
const benchmark = ({
runCount = 10_000_000,
getArgs = () => [],
testFunctions = {},
}) => {
const args = getArgs(); // Alternative 1
const test = (fn) => {
const args = getArgs(); // Alternative 2
const start = performance.now();
for (let i = 0; i < runCount; i++) {
const args = getArgs(); // Current Location
fn(...args);
}
const end = performance.now();
const runtime = end - start;
return runtime;
};
const results = {};
for (const funcName in testFunctions) {
const fn = testFunctions[funcName];
results[funcName] = test(fn);
}
return results;
};
Alternative 1
The first alternative is to call getArgs
outside of the test
function. By doing this, all of your tests will use the same set of arguments. However, this increases the chances that the JavaScript engine will perform optimizations that skew your test results. For example, it may realize that the arguments are constant and that the function is deterministic, and therefore cache the function return value. Also, this means your test functions don't have a chance to actually run against a variety of arguments, which would better represent their performance in a production setting.
Alternative 2
The second alternative is to call getArgs
right before starting the timer for each function. This might decrease the chances of the engine performing its own unrealistic optimizations, but it still doesn't provide a variety of inputs for your functions to represent the unpredictable nature of a production environment.
My Pick
Overall, I prefer the current location of getArgs
and I think it helps ensure the test results are representative of which function will have better performance in production.