[SalesForce] Array.find and Array.findIndex has worst performance in LWC Lightning Locker

For the component chain like below:

<container>
    <parent>
        <child>
            <grand-child>

Pass an Array from container to all the way down to grand-child. This is how we should communicate down the hierarchy according to LWC best practices. When we use any array function, the performance is excellent when used in parent but is worst and adversely affected when in grand-child and worst of all the functions affected are find and findIndex

Consider the example in this playgroundThis will work fine in playground as locker service is disabled but when you test in any dev org/sandbox, you will notice the difference

  1. Click on Load to load array
  2. Click on test buttons to check performance in parent, child and grand-child.

Below are the results:

enter image description here

I found the solution by using JSON.parse and JSON.stringify or else if its only single level array objects then by using shallow copy by Spread syntax. However, we are not supposed to implement our own work-around and so considering this as bug in LWC.

Why are all array functions multi-fold affected with each passing level? And why specially find and findIndex are affected much worse?
Is this known issue? If so, is there a fix for this in road-map?


Copying the playground example here as they will be deleted:

app.html:

<template>
    <lightning-button label="Load" onclick={load}>
    </lightning-button>
    <lightning-button label="Test Main" onclick={test}>
    </lightning-button>

    <c-performance-child1 myarray={myarray}></c-performance-child1>
</template>

app.js:

import PerformanceInherit from 'c/performanceInherit';

export default class App extends PerformanceInherit { }

performanceInherit.js:

import { LightningElement, track, api } from 'lwc';

export default class PerformanceInherit extends LightningElement {
    compId = 'parent';

    @api myarray = [];

    load() {
        let myarray = [];
        for (let i = 1; i < 111; i++) {
            myarray.push('elem ' + i);
        }
        this.myarray = myarray;
    }

    test() {
        let t1 = new Date().getTime();
        this.myarray.find(item => false);
        let t2 = new Date().getTime();
        console.log('find ______', this.compId + ' => ', (t2 - t1) + ' ms');

        let t3 = new Date().getTime();
        this.myarray.forEach(item => item);
        let t4 = new Date().getTime();
        console.log('forEach ___', this.compId + ' => ', (t4 - t3) + ' ms');

        let t5 = new Date().getTime();
        this.myarray.filter(item => false);
        let t6 = new Date().getTime();
        console.log('filter ____', this.compId + ' => ', (t6 - t5) + ' ms');

        let t7 = new Date().getTime();
        this.myarray.map(item => item);
        let t8 = new Date().getTime();
        console.log('map _______', this.compId + ' => ', (t8 - t7) + ' ms');

        let t9 = new Date().getTime();
        this.myarray.findIndex(item => false);
        let t10 = new Date().getTime();
        console.log('findIndex _', this.compId + ' => ', (t10 - t9) + ' ms');

        console.log('_________________', this.myarray.length);
    }
}

performanceChild1.html

<template>
    <lightning-button label="Test Parent"
                      onclick={test}>
    </lightning-button>

    <c-performance-child2 myarray={myarray}></c-performance-child2>
</template>

performanceChild1.js

import PerformanceInherit from 'c/performanceInherit';

export default class PerformanceChild1 extends PerformanceInherit { }

performanceChild2.html

<template>
    <lightning-button label="Test Child"
                      onclick={test}>
    </lightning-button>

    <c-performance-child3 myarray={myarray}></c-performance-child3>
</template>

performanceChild2.js

import PerformanceInherit from 'c/performanceInherit';

export default class PerformanceChild2 extends PerformanceInherit {
    constructor() {
        super();
        this.compId = 'child';
    }
}

performanceChild3.html

<template>
    <lightning-button label="Test Grand-Child"
                      onclick={test}>
    </lightning-button>
</template>

performanceChild3.js

import PerformanceInherit from 'c/performanceInherit';

export default class PerformanceChild3 extends PerformanceInherit {
    constructor() {
        super();
        this.compId = 'grand-child';
    }
}

Best Answer

It due to LockerService overriding the getters on Arrays when it's wrapped in a Proxy. There is a function called getFilteredArray(st, raw, key) which is called every time you try to access a property on an array. Here is the source code for it:

function getFilteredArray(st, raw, key) {
    const filtered = [];
    // TODO: RJ, we are missing named(non-integer) properties, changing this for loop to for..in should fix it
    for (let n = 0; n < raw.length; n++) {
      const value = raw[n];
      let validEntry = false;
      if (
        !value || // Array can contain undefined/null/false/0 such falsy values
        getKey(value) === key // Value has been keyed and belongs to this locker
      ) {
        validEntry = true;
      } else {
        const filteredValue = filterEverything(st, value, { defaultKey: key });
        if (filteredValue && !isOpaque(filteredValue)) {
          validEntry = true;
        }
      }
      if (validEntry) {
        // Store the raw index and value in an object
        filtered.push({ rawIndex: n, rawValue: value });
      }
    }

    return filtered;
  }

This function loops through your entire array every time you try to access an element in your array. So if you loop through your array a certain way (e.g. for(...) then what you expect to be an O(n) operation, actually becomes a O(n^2) operation.

You can see the rest of the Proxy handler if you look for the aura_proddebug.js file and look for function getArrayProxyHandler(key). You will need to have debug mode on for you user.

Related Topic