Lightning Web Components – Improve Performance When Iterating API Variable on Nested LWC

lightning-web-componentsperformance

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

Before copy implementation

After

After copy implementation

The difference is that we copy the values before iterating over them:

grandchild.js

import { LightningElement, api } from 'lwc';

export default class Grandchild extends LightningElement {
    _foos;
    @api set foos(value) {
        this._foos = [...value];
    }
    get foos() {
        return this._foos;
    }
    output;
    handleClick() {
        const startTime = performance.now();
        this.foos.map(foo => foo);
        const endTime = performance.now();

        console.log(this.output = `Map call took ${endTime - startTime} milliseconds`);
    }
}

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.

Related Topic