Users expect web applications to be fast and responsive; code size is one consideration when loading applications, but the code's performance, once loaded, significantly impacts the user experience.
Benchmarking your code is an important tool for identifying issues and bottlenecks that can be addressed to improve performance and user experience.
When working on a large codebase with multiple developers contributing, it can be challenging to understand the impact on the overall performance minor code changes are contributing. Benchmarking your entire code base is helpful when determining performance changes over time, but it's not particularly useful when trying to correct particular performance issues and understand what approach or change will result in the best performance.
To address performance issues, you need to be able to isolate specific sections of your code so you can benchmark its baseline performance and measure the impact of any changes you make.
Using console.time() & console.timeEnd()
One of the simplest ways to benchmark isolated sections of your code is by wrapping the section in the console.time()
and console.timeEnd()
methods and measuring the time it takes to traverse the code between the two methods.
function slowFunction(array) {
let result = [];
for (let i = 0; i < array.length; i++) {
result[i] = Math.pow(array[i], 2);
for (let j = 0; j < 10000000; j++) {
// do nothing
}
}
return result;
}
const sampleArray = [1, 2, 3, 4, 5];
console.time("slowFunction");
slowFunction(sampleArray);
console.timeEnd("slowFunction");
The console.time()
method starts a timer with the label provided, in this example, "slowFunction"
, but it could be anything you want and the console.timeEnd()
method ends the time with the same label and outputs the elapsed time between the console.time()
call in milliseconds.
So for the example above, it would measure the time taken to execute slowFunction();
Depending on the hardware and environment the code runs on, slowFunction()
could take ~22ms
to execute. Now if we were to optimise this function and run it again, we would get a much better result. In this example, something around ~1ms.
function slowFunction(array) {
let result = new Array(array.length);
for (let i = 0; i < array.length; i++) {
result[i] = array[i] * array[i];
}
return result;
}
const sampleArray = [1, 2, 3, 4, 5];
console.time("slowFunction");
slowFunction(sampleArray);
console.timeEnd("slowFunction");
Using performance.now()
It's often important to be able to record the output of a test if you are looking to store or do something with the value, instead of just outputting the result to the console.
The performance.now()
method works in a similar way to console.time()
and console.timeEnd()
. A call to performance.now()
returns the number of milliseconds elapsed since the start of the current execution. When a section of code is wrapped between two calls to performance.now()
, the value of the second call can be subtracted from the first to calculate the number of milliseconds elapsed between the two.
function slowFunction(array) {
let result = [];
for (let i = 0; i < array.length; i++) {
result[i] = Math.pow(array[i], 2);
for (let j = 0; j < 10000000; j++) {
// do nothing
}
}
return result;
}
const sampleArray = [1, 2, 3, 4, 5];
const startTime = performance.now();
slowFunction(sampleArray);
const endTime = performance.now();
console.log(`Time taken to execute is ${endTime - startTime} ms.`);
It's important to note that to protect against timing attacks and fingerprinting, the precision of performance.now()
might get rounded depending on browser settings and could cause some inaccuracy in results.
Accounting for JavaScript engine warm up
When you first run a piece of code, it may take longer to execute than the subsequent runs due to several factors, including just-in-time (JIT) compilation or caching. This can taint the result of your benchmark testing.
For example, if we run our above example twice, we would get a different result on the second execution.
function slowFunction(array) {
let result = [];
for (let i = 0; i < array.length; i++) {
result[i] = Math.pow(array[i], 2);
for (let j = 0; j < 10000000; j++) {
// do nothing
}
}
return result;
}
const sampleArray = [1, 2, 3, 4, 5];
let startTime = performance.now();
slowFunction(sampleArray);
let endTime = performance.now();
console.log(`1st execute is ${endTime - startTime} ms.`);
startTime = performance.now();
slowFunction(sampleArray);
endTime = performance.now();
console.log(`2nd execute is ${endTime - startTime} ms.`);
To get a more accurate indication of functions performance, running your benchmark code multiple times within the same execution can be useful, allowing the JavaScript engine to warm up and reach a steady state.
To make this a little cleaner and avoid copy-pasting the test multiple times, we can wrap our benchmarking code in a new benchmark() function that can manage running our function multiple times and returning the average execution time in milliseconds.
This function uses Function.prototype.apply()
to call our function multiple times with the same parameters. It accepts three arguments:
func
: The function to be benchmarked.args
: An array of arguments to be passed to the test function.iterations
: The number of times to run the function (defaults to 1000).
Using our new benchmark()
function, you could benchmark any function in your code like this.
function slowFunction(array) {
let result = [];
for (let i = 0; i < array.length; i++) {
result[i] = Math.pow(array[i], 4);
for (let j = 0; j < 10000000; j++) {
// do nothing
}
}
return result;
}
const sampleArray = [1, 2, 3, 4, 5];
const averageTime = benchmark(slowFunction, [sampleArray], 300);
console.log(`Time taken to execute is ${averageTime} ms.`);
If you want to test the preformance of a larger section of code, or multiple function calls, you can wrap them in an anonomous function.
const sampleArray = [1, 2, 3, 4, 5];
const averageTime = benchmark(() => {
functionOne(sampleArray);
functionTwo(sampleArray);
}, [], 5);
console.log(`Time taken to execute is ${averageTime} ms.`);
To avoid copy-pasting our custom function all over your code, every time you want to benchmark something, it can be helpful to create a new file to store your benchmark() function and include it in code when you want to use it.
Using Benchmark Libraries
If you want to avoid creating your own function altogether, you can use some popular benchmarking libraries like Benchmark.js.
Benchmark.js provides a simple API you can use whenever you want to benchmark functions or sections of your code.
The API requires a few additional steps than our customer benchmark() function, but we could test the above code like so.
const Benchmark = require('benchmark');
// Define the function to be benchmarked
function slowFunction(array) {
let result = [];
for (let i = 0; i < array.length; i++) {
result[i] = Math.pow(array[i], 2);
for (let j = 0; j < 10000000; j++) {
// do nothing
}
}
return result;
}
// Create a new benchmark suite
const suite = new Benchmark.Suite;
// Add the function to the suite
suite.add('slowFunction', function() {
slowFunction([1, 2, 3, 4, 5]);
})
// Add a listener for the 'complete' event
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// Run the benchmark
.run({ 'async': true });
Benchmark.js will execute the test code multiple times, accounting for JavasScript warmup time in a similar way to our custom function. It will also allow you to compare the results of other functions in a single test run by adding them to the suite, allowing you to determine the fastest option quickly. You can read more about how to use Benchmark.js co compaire performance in their documentation.
Benchmark methodology
There are many ways to benchmark code, and your methodology can affect the results. You might choose to measure the time taken to execute a single function call, or you might decide to measure the time taken to execute a series of functions and ad-hock code.
The choice of methodology depends on what you are trying to measure and what you consider could affect the performance. The critical thing to remember when comparing performance is consistency; if you are comparing one test with another, you need to ensure the methodology is consistent between the two tests and you are only swapping out sections of code between tests that you are looking to understand the performance on.