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](https://i.stack.imgur.com/jqq5O.png)
After
![After copy implementation](https://i.stack.imgur.com/FppYN.png)
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.
Considering your scenario, I tried creating some tree structure on my dev org.
Below is the code snippet for all the components:
dynamicTreeStructure.html
<template>
<lightning-card title="Tree Components with mutliple nested Accounts">
<div class="slds-m-top_medium slds-m-bottom_x-large">
<!-- Simple -->
<template if:true={accounts.data}>
<div class="slds-p-around_medium lgc-bg">
<lightning-tree items={accounts.data} header="Accounts"></lightning-tree>
</div>
</template>
</div>
</lightning-card>
dynamicTreeStructure.js
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/DynamicTreeStructureController.getAccounts';
export default class Dynamic_Tree_Structure extends LightningElement {
@wire(getAccounts) accounts;
}
DynamicTreeStructureController.cls
public with sharing class DynamicTreeStructureController {
public DynamicTreeStructureController() {
}
private static Map<Id, TreeStructure> result;
private static Map<Id, Id> childIdMap;
/**
Static Method to be fed in @wire for LWC
*/
@AuraEnabled(cacheable=true)
public static List<TreeStructure> getAccounts(){
result = new Map<Id, TreeStructure>();
childIdMap = new Map<Id, Id>();
Map<Id, Account> accMap = new Map<Id, Account>([SELECT Id, Name FROM Account WHERE ParentId = null]);
if(!accMap.isEmpty()){
startFetchingAccountDetails(accMap);
}
System.debug(JSON.serialize(result));
return result.values();
}
/**
* Recursion method to get all levels of accounts and their related records
*/
private static List<TreeStructure> startFetchingAccountDetails(Map<Id, Account> accMap){
Map<Id, TreeStructure> parentStructure = gatherAllAccountInformation(accMap);
//attach the first level to actual result and rest will auotmatically link
//due to pass by reference way
if(result == null || result.isEmpty()){
result.putAll(parentStructure);
}
Map<Id, Account> childMap = new Map<Id, Account>([SELECT Id, Name, DemoLight12__Account__c FROM Account WHERE DemoLight12__Account__c =: accMap.keySet()]);
if(childMap != null && !childMap.isEmpty() && childMap.size() > 0){
Map<Id, Id> accChildIdMap = new Map<Id, Id>();
for(Id childAccountId : childMap.keySet()){
Account child = childMap.get(childAccountId);
childIdMap.put(child.Id, child.DemoLight12__Account__c);
}
//run this method recursively to get all child levels.
List<TreeStructure> childStructure = startFetchingAccountDetails(childMap);
for(TreeStructure child : childStructure){
TreeStructure parent = parentStructure.get(childIdMap.get(child.name));
parent.items.add(child);
}
}
return parentStructure.values();
}
/**
* Method to gather all information for all accounts recieved
*/
private static Map<Id, TreeStructure> gatherAllAccountInformation( Map<Id, Account> accMap){
Map<Id, TreeStructure> result = new Map<Id, TreeStructure>();
Map<Id, List<Contact>> accConMap = new Map<Id, List<Contact>>();
Map<Id, List<Opportunity>> accOppCMap = new Map<Id, List<Opportunity>>();
Map<Id, List<Case>> conCaseCMap = new Map<Id, List<Case>>();
//gather all contacts
for(Contact con : [SELECT Id, Name, AccountId FROM Contact WHERE AccountId =: accMap.keySet()]){
if(!accConMap.containsKey(con.AccountId)){
accConMap.put(con.AccountId, new List<Contact>());
}
accConMap.get(con.AccountId).add(con);
}
//gather all cases
for(Case cas : [SELECT Id, CaseNumber, ContactId FROM Case WHERE ContactId =: accConMap.keySet()]){
if(!conCaseCMap.containsKey(cas.ContactId)){
conCaseCMap.put(cas.ContactId, new List<Case>());
}
conCaseCMap.get(cas.ContactId).add(cas);
}
for(Id accountId : accMap.keySet()){
Account acc = accMap.get(accountId);
TreeStructure accStructure = new TreeStructure(acc.name, accountId, false, null);
//add all contacts if present
if(accConMap.containsKey(accountId)){
TreeStructure conStructure = new TreeStructure('Contacts', 'Contacts', false, null);
for(Contact con : accConMap.get(accountId)){
conStructure.items.add( new TreeStructure(con.Name, con.Id, false, null));
if(conCaseCMap.containsKey(con.Id)){
TreeStructure caseStructure = new TreeStructure('Cases', 'Cases', false, null);
for(Case cas : conCaseCMap.get(con.Id)){
caseStructure.items.add( new TreeStructure(cas.CaseNumber, cas.Id, false, null));
}
conStructure.items.add(caseStructure);
}
}
accStructure.items.add(conStructure);
}
result.put(accountId, accStructure);
}
return result;
}
}
Wrapper - TreeStructure.cls
public class TreeStructure{
@AuraEnabled public String label;
@AuraEnabled public String name;
@AuraEnabled public Boolean expanded;
@AuraEnabled public List<TreeStructure> items;
public TreeStructure(String label, String name, Boolean expanded, List<TreeStructure> items){
this.label = label;
this.name = name;
this.expanded = expanded;
if(items != null && items.size() > 0){
this.items = items;
}else{
this.items = new List<TreeStructure>();
}
}
}
![enter image description here](https://i.stack.imgur.com/rUXQP.png)
Hope, I understood your use case properly and this helps you.
Best Answer
A component can reference itself recursively. I previously wrote this in Aura, but the principle remains the same.
First, we just translate the markup to a LWC template:
The corresponding CSS for this doesn't change much:
Nor does the controller logic:
You can see the full code on the linked answer if you need additional help from there.
Because of the requirement to have a key, you'll want to generate keys or use record Id values, whichever is acceptable for your use case.
Demo.