17

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 playground - This 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';
    }
}
4
  • I suspect it is more to do with proxies than the locker service directly. Every API and tracked property value gets proxified to ensure reactive handling and I suspect this happens even when the value is already a proxy (so access to the underlying value becomes more and more nested - I am speculating here). Perhaps this is compounded by some aspect of the locker service? – Phil W Feb 26 '20 at 7:27
  • @PhilW, Yes! this is what I too suspected and so put locker service in question heading directly. – salesforce-sas Feb 26 '20 at 7:40
  • 1
    it might be a good idea to copy out the playground example into the question before Salesforce kills stored playground examples. – Phil W Jul 31 '20 at 7:30
  • @PhilW.. Thanks so much! – salesforce-sas Jul 31 '20 at 7:53
13

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.

1
  • 10
    Seems that @salesforce-sas is right and this should be considered a bug; at the very least it is poor programming in the Lightning infrastructure. – Phil W Feb 26 '20 at 22:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.