28

Given a super simple scenario loading a list of 2000 Strings via Apex controller. When I process the list in a Lightning Component, there is a huge difference in performance between API v39 and v40. Here is a simplified example:

MyDataController.cls:

 public with sharing class MyDataController {
    @AuraEnabled
    public static String[] getStringData() {
      String[] myDataList = new List<String>();
      for(Integer i = 0; i < 2000; i++) {
        myDataList.add('element_' + i);
      }
      return myDataList;
    }
  }


TestApplication.app:

<aura:application controller="MyDataController">
  <aura:attribute name="myDataList" type="String[]" />

  <aura:handler name="init" value="{!this}" action="{!c.onInit}" />

  <button onclick="{!c.onButtonClick}">Click</button>
</aura:application>


TestApplicationController.js:

({
    onInit: function(component, event, helper) {
        var action = component.get("c.getStringData");
        action.setCallback(this, function(response) {
            if (response.getState() === "SUCCESS") {
                component.set("v.myDataList", response.getReturnValue());
            }
        });

        $A.enqueueAction(action);
    },

    onButtonClick: function(component, event, helper) {
        var dataList = component.get("v.myDataList");
        var element;
        var i = 0;
        console.time("loop_time");
        for(i; i < 2000; i++) {
            element = dataList[i];
        }
        console.timeEnd("loop_time");
    }
})

So clicking the button measures the loop execution time, where I literally do nothing then accessing each element - no further logic, no events, no rendering occuring.

With API v39 this is super fast as expected (multiple clicks to validate time): enter image description here

But when I change both classes to API v40 this gets dramatically slower: enter image description here

How could this be?
Even if this would relate to LockerService this does not make sense since I only have a plain list with strings.
Interesting fact: when I create the same kind of list in the init-method with pure JavaScript and store it back in the attribute I don't see a performance loss when clicking the button. Lightning Component Debug mode has been disabled all time.

  • Updated my answer with a verified working solution. It basically sidesteps Locker Service by storing the data as a serialised string. Because it's the getters and setters in Locker Service that are imposing that 200x slowdown. – Caspar Harmer Jun 29 '17 at 9:58
  • Have you heard much from support team? I'd suspect this has something to do with Proxy objects. Also, for loops are sort of not very useful in modern JS. – dzh Jul 19 '17 at 2:02
9

Ok, this was interesting.

I was able to optimize a little... then a lot.

First optimization: I used a forEach loop. This took the time from about 700ms to 7-14ms.

Eg

onButtonClick: function(component, event, helper) {
    var dataList = component.get("v.myDataList");
    var element;
    console.time("loop_time");
    dataList.forEach(function(item){
        element = item;
    });
    console.timeEnd("loop_time");
}

EDIT My third attempt works. The second attempt was a red herring, as I had the wrong API version set while testing.

Essentially, it involves getting around the built in Locker Service getters and setters by storing the data as a serialised string.

So, this will fix it (albeit in an annoying, parse it yourself kind of way)

Apex Controller:

@AuraEnabled
public static String getStringData() {
  String[] myDataList = new List<String>();
  for(Integer i = 0; i < 2000; i++) {
    myDataList.add('element_' + i);
  }
  return JSON.serialize(myDataList);
}

Attribute list is stored as a single string:

<aura:attribute name="myDataString" type="String" default="" />

Javascript init:

onInit: function(component, event, helper) {
    var action = component.get("c.getStringData");
    action.setCallback(this, function(response) {
        if (response.getState() === "SUCCESS") {
             component.set("v.myDataString", response.getReturnValue());
        }
    });
    $A.enqueueAction(action);
},

Click handler:

 onButtonClick: function(component,event,helper) {
    var temp = component.get("v.myDataString");
    var dataList = JSON.parse(temp);
    var element;
    console.time("loop_time");
    dataList.forEach(function(item){
        element = item;
    });
    console.timeEnd("loop_time");
}

I have checked the API version and I can confirm that this blazing fast again! However, of course it's a pain. I guess you'd use it in situations where you have a large list.

  • Thank you Caspar, I don't see performance improvements be using JSON serialization - BUT Array.forEach() is indeed super fast in this context. But in my opinion there must be a technical issue in Lightning Components as a default for-loop in plain JavaScript should not be significant slower then Array.forEach. – Christian Menzinger Jun 29 '17 at 8:51
  • You are right. I must have had api version 39 set while testing by accident, sorry. I have another solution for you that works though - just writing it up now. – Caspar Harmer Jun 29 '17 at 9:50
  • 1
    This is an awesome workaround you found Caspar, appreciate. I leave the post open for a while as I think all the facts we found might be useful for a review by the Lightning Components and/or Locker Service team members. – Christian Menzinger Jun 29 '17 at 13:26
2

This has recently been recognised as known issue by Salesforce.

Link: https://success.salesforce.com/issues_view?id=a1p3A000000mCDEQA2&title=lightning-performance-difference-is-seen-when-the-apex-controller-returns-typed-object-compared-to-returning-a-serialized-object-string

  • 2
    It says resolved now, but I don't think it's right. Just tried to sort 50k records using lodash in vanilla environment and Lightning. It should take around 50-200ms while Lightning tab crashes completely. – dzh Feb 13 '18 at 4:34

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.