Performance of jQuery.grep vs. Array.filter

22,246

Solution 1

As found on this blog post (which also does the same kind of tests):

If you read the documentation for filter, you will see why it's so much slower.

  1. It ignores deleted values and gaps in the array
  2. It optionally sets the execution context of the predicate function
  3. It prevents the predicate function from mutating the data

Solution 2

Section 15.4.4.20 of the ECMAScript 5.1 spec defines Array.prototype.filter(callbackfn, thisArg) as follows:

callbackfn should be a function that accepts three arguments and returns a value that is coercible to the Boolean value true or false. filter calls callbackfn once for each element in the array, in ascending order, and constructs a new array of all the values for which callbackfn returns true. callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.

If a thisArg parameter is provided, it will be used as the this value for each invocation of callbackfn. If it is not provided, undefined is used instead.

callbackfn is called with three arguments: the value of the element, the index of the element, and the object being traversed.

filter does not directly mutate the object on which it is called but the object may be mutated by the calls to callbackfn.

The range of elements processed by filter is set before the first call to callbackfn. Elements which are appended to the array after the call to filter begins will not be visited by callbackfn. If existing elements of the array are changed their value as passed to callbackfn will be the value at the time filter visits them; elements that are deleted after the call to filter begins and before being visited are not visited.

That in itself is already a lot of work; a lot of steps that the ECMAScript engine needs to perform.

Then it goes on to say the following:

When the filter method is called with one or two arguments, the following steps are taken:

Let O be the result of calling ToObject passing the this value as the argument. Let lenValue be the result of calling the [[Get]] internal method of O with the argument length. Let len be ToUint32(lenValue). If IsCallable(callbackfn) is false, throw a TypeError exception. If thisArg was supplied, let T be thisArg; else let T be undefined. Let A be a new array created as if by the expression new Array() where Array is the standard built-in constructor with that name. Let k be 0. Let to be 0. Repeat, while k < len Let Pk be ToString(k). Let kPresent be the result of calling the [[HasProperty]] internal method of O with argument Pk. If kPresent is true, then Let kValue be the result of calling the [[Get]] internal method of O with argument Pk. Let selected be the result of calling the [[Call]] internal method of callbackfn with T as the this value and argument list containing kValue, k, and O. If ToBoolean(selected) is true, then Call the [[DefineOwnProperty]] internal method of A with arguments ToString(to), Property Descriptor {[[Value]]: kValue, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false. Increase to by 1. Increase k by 1. Return A.

The length property of the filter method is 1.

NOTE The filter function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method. Whether the filter function can be applied successfully to a host object is implementation-dependent.

Some things to note about this algorithm:

  • It prevents the predicate function from mutating the data
  • It optionally sets the execution context of the predicate function
  • It ignores deleted values and gaps in the array

In a lot of cases, none of these things are needed. So, when writing a filter method of your own, most of the time you wouldn’t even bother to perform these steps.

Every ES5.1-compliant JavaScript engine must conform to that algorithm, and must thus perform all those steps every time you use Array#filter.

It should be no surprise that any custom-written method that only performs a part of those steps will be faster :)

If you write your own filter function, chances are it’s not gonna be as complex as the above algorithm. Perhaps you won’t be converting the array to an object at all, as depending on the use case it may not be needed just to filter the array.

Solution 3

I found out something interesting. As explained by MarcoK, $.grep is just a simple implementation with a for loop. Filter is slower in most cases, so the implementation must be different. I think i found the answer:

function seak (e) { return e === 3; }

var array = [1,2,3,4,5,6,7,8,9,0], i, before;
array[10000] = 20; // This makes it slow, $.grep now has to iterate 10000 times.
before = new Date();

// Perform natively a couple of times.
for(i=0;i<10000;i++){
    array.filter(seak);
}

document.write('<div>took: ' + (new Date() - before) + '</div>'); // took: 8515 ms (8s)

before = new Date();

// Perform with JQuery a couple of times
for(i=0;i<10000;i++){
    $.grep(array, seak);
}
document.write('<div>took: ' + (new Date() - before) + '</div>'); // took: 51790 ms  (51s)

The native 'filter' is much faster in this case. So i think it iterates the properties rather than the array index.

Now lets get back to 'big' problems ;).

Solution 4

Isn't your script wrong?

For array.filter you are doing the measurement 1000 times and present it by taken the sum divided by 1000

For JQuery.grep you are doing the measurement 1 time and present it by taken the sum divided by 1000.

That would mean your grep is actually 1000 times slower than the value you use for comparison.

Quick test in firefox gives:

Machine 1:
average filter - 3.864
average grep - 4.472

Machine2:
average filter - 1.086
average grep - 1.314

Quick test in chrome gives:

Machine 1:
average filter - 69.095
average grep - 34.077

Machine2:
average filter - 18.726
average grep - 9.163

Conclusion in firefox (50.0) is a lot faster for your code path, and filter is about 10-15% faster than jquery.grep.

Chrome is extremely slow for your code path, but grep seems to be 50% faster than array.filter here making it 900% slower than the firefox run.

Share:
22,246
Christoph
Author by

Christoph

I love coding and am excited to see how the web evolves around us at such rapid speed.

Updated on November 21, 2020

Comments

  • Christoph
    Christoph over 3 years

    In a question it was discussed on how jQuery and native JS would perform against each other.

    While of course the vanilla solution performs a lot faster because it does not process the whole array I proposed the usage of Array.filter which I was pretty confident would be at least faster than $.grep.

    Surprisingly after adding it to the test I was taught a lesson: Testsuite

    Edgecases of course have a different outcome.

    Anyone having an idea why $.grep is supposed to be over 3 times faster than the native method Arrray.filter?

    Edit: I modified the test to use the filter shim from MDN and the results are pretty interesting:

    • Chrome: Even MDN shim is faster than the native method, jQuery way ahead
    • Firefox: shim a bit slower than native method, jQuery way ahead

    and finally a result like i was hoping it to see in

    • Internet Explorer: native method is the fastest, then jQuery, shim is slowest (perhaps this is just the result of IEs rather weak JS-engine...)