You might recall your first Hello World tutorial on Lightning Web Components. In those simple days, there were really only a few things you could mess up, and soon enough, you were moving on to templates, iterators, and wire functions. However, there are still a few gotchas with the LWC basics, and I ran into a particularly peculiar one the other day. It went a little like this.

I was writing a seemingly simple feature in a not-as-simple project, but basically, I expected a hundred or so divs to render with some attached styling and data. I probably wouldn’t have noticed anything amiss were it not for the fact that it took nearly 10 seconds to render just 150 divs in a relatively flat layout. If you open up your Inspector console (hit F12), you probably have a couple hundred divs on any given page, yet they don’t take very long to load at all. So what happened?

Taking a Closer Look

It turns out, there is a “feature” hidden within LockerService (it’s always LockerService) that considerably affects the performance of array access, especially in nested components. Consider this scenario:

You have a container component that fetches hierarchical Account data with associated Contacts. It renders the data in a child component that represents a tree node. The payload looks like this:

[
{
Id: ‘bbb’,
Name: ‘CodeScience’,
ParentId: ‘aaa’,
Contacts: [
{Id: ‘xxx’, Name: ‘Jalal Khan’},
{Id: ‘yyy’, Name: ‘Jalal Khan Jr.’},
{Id: ‘zzz’, Name: ‘Jalal Khan III’}
]
},

]

Now let’s say we want to display this data in a tree structure, represented in my world class ASCII art below:

  Parent Account – CEO
CTO
CodeScience – Jalal Khan
Jalal Khan Jr
Jalal Khan III

We want some special styling applied to every other element in the list, so we’ll have to write a getter in the child component like so:

get customStyleClass() {
const account = this.accountData.find(account => account.Id == this.account.Id);
const contactIdx = account.contacts.findIndex(contact => contact.Id == this.contact.Id);
return idx % 2 === 0 ? ‘special-style’ : ‘normal-style’;
}

Not the best performance, I know, but this is just an example. It shouldn’t be that awful for just 150 divs, right? Wrong. Let’s start tracking the cyclical complexity of our use case.

Where Things Get Blurry

When we render all our child components, we use a template iterator. That is an O(n) operation.

<template for:each={accounts} for:item=”account”>

Now, in our customStyle getter, we are doing another iteration of the same array, so the operation becomes O(n^2) just to render any particular component. Now, most computers these days are fast enough to execute a lot of javascript without much lag, so I’m not going to start worrying about performance just yet. Unfortunately for us, LockerService introduces some additional overhead that balloons our performance problem.

To understand what happens, we need to dig into LockerService. LockerService works by wrapping all the properties in your component in a Proxy object. Now, when we want to access an array “property”, like literally just any element in the array or a function in the prototype, we have to go through LockerService first, which makes sure what we’re doing is kosher. Incidentally, LockerService iterates over the entire array before returning our value of interest. It looks like this:

function getFilteredArray(st, raw, key) {
const filtered = [];
for (let n = 0; n < raw.length; n++)
const value = raw[n];
let validEntry = false;
if (!value || getKey(value) === key) {
validEntry = true;
} else {
const filteredValue = filterEverything(st, value, { defaultKey: key });
if (filteredValue && !isOpaque(filteredValue)) {
validEntry = true;
}
}
if (validEntry) {
filtered.push({ rawIndex: n, rawValue: value });
}
}

    return filtered;
}

This adds another complication to our already iffy scenario. This function looks for our value of interest, but does so by looping over the entire input array! What happened to O(1) access? Now it’s O(n) access.

Seeing the Solution

If you’re still keeping track, we’re somewhere in the ballpark of O(n^3) just to render a nested Contact name! Fortunately, a StackOverflow sleuth has run into this same problem and conducted an experiment to indicate the ramifications: https://salesforce.stackexchange.com/q/295924

As salesforce-sas has detailed for us, array function performance gets railroaded thanks to Proxy objects. Now we’re all done, right? But wait, they’ve left with an unanswered question: why do `find` and `findIndex` perform so much worse than other array functions?

`findIndex` is a native Javascript function that matches on the first array element with the provided condition expression and returns the index of that element. LockerService has this beautiful snippet when you try to execute any array function:

get: function(target, property) {

if(typeof property === ‘number’) {
ret = getFromFiltered(handler, filtered, property);
} else {
switch(property) {
case ‘length’:…
case ‘pop’:
case: ‘push’

}
}

A nice long list of array prototype functions. Which happens to not include a case for `find` or `findIndex`, by the way. So it defaults into this:

default: ret = filterEverything(handler, raw[property]);

Which ultimately performs another loop on the array function properties until it finds our function of interest: `findIndex`. All these loops are making me dizzy now. It turns out that our current complexity is actually O(n^3 + m^2). This is a bit of a special case with `find` and `findIndex`. Using `filter` instead of `findIndex` reduces it down to O(n^3 + m), only marginally better, but will have a very real effect on usability, particularly with lower count array sizes.

The takeaway here is that we should avoid passing down an array and iterating over it in a child component. Some of this is common sense, but I would argue there are some very real use cases for doing such a thing, such as constructing a hierarchical tree or accessing deeply nested data that is paralleled in your presentation. You may also want to perform a calculation on related nodes inside your child component, so it would make sense to just execute it within the nested component instead of precalculating it, when you’ll have to iterate over the array again anyways.

The workaround I used was to key my accounts by their record id and pass in a map alongside my array to completely avoid the array altogether, but still perform my calculations in the child component. This improved my performance dramatically and was probably a better idea in the first place. You may also wish to normalize or precompute your data so you don’t have to do any additional iterations, which should still be ok for a single level child.

One caveat is that the problem I’ve outlined compounds with the more levels of children you have. Having a container -> child -> grandchild structure will have absolutely awful performance at the grandchild level. You will definitely need a workaround even with a 50 element array at that point!

Good luck and happy coding!


CodeScience has helped build more than 10% of the apps on the AppExchange. Contact us today to learn how we can help your business thrive.