I am experiencing a weird behaviour when iterating a relatively big list in an lwc api variable when this is going through nested components. When I have two nested components the time taken to iterate the list is significantly slower than the same operation when there is only one nested component.
In both cases a parent component is fetching a list of dtos using imperative Apex. I have a button on the last component just to initiate a .map
call to the list of dtos.
First example
Time taken: 17 seconds
The parent provides the list to the child component through an api variable which in turn passes it through an other api variable to the grandchild component.
Second example
Time taken: 133 milliseconds
The parent provides the data to the grandchild component through an api.
Below is the code for the components:
For the second example I just modify the parent.html
by changing <c-child foos={foos}></c-child>
to <c-grandchild foos={foos}></c-grandchild>
public with sharing class Controller {
public class Foo {
@AuraEnabled
public String bar {get; set;}
}
@AuraEnabled
public static List<Foo> getFoos(){
List<Foo> foos = = new List<Foo>();
for(Integer i=0; i<4000; i++) {
Foo foo = new Foo();
foo.bar = 'foo' + i;
foos.add(foo);
}
return foos;
}
}
// Parent.js
import { LightningElement, track } from 'lwc';
import getFoos from '@salesforce/apex/Controller.getFoos';
export default class Parent extends LightningElement {
foos;
get foosAvailable() {
return this.foos !== undefined;
}
connectedCallback() {
getFoos()
.then((data) => {
this.foos = data;
});
}
}
<!-- parent.html -->
<template>
<lightning-card>
<template if:false={foosAvailable}>
<div class="slds-var-m-around_medium">Foos loading...</div>
</template>
<template if:true={foosAvailable}>
<c-child foos={foos}></c-child>
</template>
</lightning-card>
</template>
// child.js
import { LightningElement, api } from 'lwc';
export default class Child extends LightningElement {
@api foos;
}
<!-- child.html -->
<template>
<c-grandchild foos={foos}></c-grandchild>
</template>
// grandchild.js
import { LightningElement, api } from 'lwc';
export default class Grandchild extends LightningElement {
@api foos;
handleClick() {
const startTime = performance.now();
this.foos.map(foo => foo);
const endTime = performance.now();
console.log(`Map call took ${endTime - startTime} milliseconds`);
}
}
<!-- grandchild.html -->
<template>
<div class="slds-var-m-around_medium">
<lightning-button
variant="brand"
label="Start"
onclick={handleClick}>
</lightning-button>
</div>
</template>
Best Answer
The problem isn't LWC, but rather Locker Service. I've explained this before, but I also wrote a new demo that shows that sub-second performance is expected. When using Locker Service, data is proxied, which can have significant performance impacts. However, this can be mitigated by copying the array:
Before
After
The difference is that we copy the values before iterating over them:
grandchild.js
In general, if you're passing arrays/objects around, and you do the appropriate copy, you'll get 100,000x performance or so.
This is a known problem, and is likely to be partially mitigated by the new Lightning Web Security, but you can definitely do yourself a favor with copy algorithms to produce a local copy. You'll use potentially a lot more memory, but you'll get insane performance boosts. You'll want to use this unless/until you discover that this is no longer an issue.
Note that this should be applied at every level of the hierarchy (e.g. every @api should copy incoming values) to avoid this problem.