29

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.

2
  • 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. Jun 29, 2017 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, 2017 at 2:02

2 Answers 2

11

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.

3
  • 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. Jun 29, 2017 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. Jun 29, 2017 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. Jun 29, 2017 at 13:26
1

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

1
  • 3
    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, 2018 at 4:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .