Search code examples
javascriptarraysperformanceecmascript-6v8

Why is the Array.prototype.find() polyfill slower than the ES6 implementation?


I was under the impression that most of the ES6 features were just syntactic sugar. However when I compare the find polyfill on MDN with the regular ES6 implementation it seems to be half as fast. What exactly explains this difference in performance, is it not all just the same under the hood?

Please refer to the snippet below for the benchmark:

// Find polyfill
function find(obj, predicate) {
  // 1. Let O be ? ToObject(this value).
  if (this == null) {
    throw new TypeError('"this" is null or not defined');
  }

  var o = Object(obj);

  // 2. Let len be ? ToLength(? Get(O, "length")).
  var len = o.length >>> 0;

  // 3. If IsCallable(predicate) is false, throw a TypeError exception.
  if (typeof predicate !== 'function') {
    throw new TypeError('predicate must be a function');
  }

  // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
  var thisArg = arguments[1];

  // 5. Let k be 0.
  var k = 0;

  // 6. Repeat, while k < len
  while (k < len) {
    // a. Let Pk be ! ToString(k).
    // b. Let kValue be ? Get(O, Pk).
    // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
    // d. If testResult is true, return kValue.
    var kValue = o[k];
    if (predicate.call(thisArg, kValue, k, o)) {
      return kValue;
    }
    // e. Increase k by 1.
    k++;
  }

  // 7. Return undefined.
  return undefined;
}
const testArray = ["Hello", "Hi", "Good Morning", "Good Afternoon", "Good Evening", "Good Night"];

// Polyfill benchmark
console.time('findPolyfill');
for (var i = 0; i < 10000; i++) {
  find(testArray, (item) => item === "Hello")
}
console.timeEnd('findPolyfill');

// ES6 benchmark
console.time('find ES6');
for (var i = 0; i < 10000; i++) {
  testArray.find((item) => item === "Hello");
}
console.timeEnd('find ES6');


Solution

  • The native version can take advantage of internal optimizations and shortcuts as long as they aren't observable from the outside. It's also likely to be pre-optimized and stored as at least bytecode if not compiled machine code. (Depends on the JavaScript engine.)

    In contrast, the polyfill is a very pedantic rendering of exactly what the spec says to do, and unless you run it more than 5-10k or so times in a tight loop, is unlikely to be selected for aggressive optimization by the engine.

    Amusingly, your loop is set to run 10k times, and so may well stop just before the engine optimizes it. Or the engine may optimize it part-way through — further delaying the result. For instance, for me, the following has the polyfill run in ~6ms the first time, but ~1.1ms the second and third times (V8 v7.3 in Chrome v73). So apparently it's getting optimized during the first run (which, perversely, probably slows that run down, but obviously speeds up subsequent ones).

    // Find polyfill
    function find(obj, predicate) {
      // 1. Let O be ? ToObject(this value).
      if (this == null) {
        throw new TypeError('"this" is null or not defined');
      }
    
      var o = Object(obj);
    
      // 2. Let len be ? ToLength(? Get(O, "length")).
      var len = o.length >>> 0;
    
      // 3. If IsCallable(predicate) is false, throw a TypeError exception.
      if (typeof predicate !== 'function') {
        throw new TypeError('predicate must be a function');
      }
    
      // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
      var thisArg = arguments[1];
    
      // 5. Let k be 0.
      var k = 0;
    
      // 6. Repeat, while k < len
      while (k < len) {
        // a. Let Pk be ! ToString(k).
        // b. Let kValue be ? Get(O, Pk).
        // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
        // d. If testResult is true, return kValue.
        var kValue = o[k];
        if (predicate.call(thisArg, kValue, k, o)) {
          return kValue;
        }
        // e. Increase k by 1.
        k++;
      }
    
      // 7. Return undefined.
      return undefined;
    }
    const testArray = ["Hello", "Hi", "Good Morning", "Good Afternoon", "Good Evening", "Good Night"];
    
    function testPolyfill() {
        // Polyfill benchmark
        console.time('findPolyfill');
        for (var i = 0; i < 10000; i++) {
          find(testArray, (item) => item === "Hello")
        }
        console.timeEnd('findPolyfill');
    }
    
    function testNative() {
        // ES6 benchmark
        console.time('find ES6');
        for (var i = 0; i < 10000; i++) {
          testArray.find((item) => item === "Hello");
        }
        console.timeEnd('find ES6');
    }
    
    testPolyfill();
    testNative();
    testPolyfill();
    testNative();
    testPolyfill();
    testNative();