Performance Tips
Storable Actions : Using action.setStorable();
Marks the actions as storable to quickly show cached data from client-side storage without waiting for a server trip.
refreshAge
: 30 secsexpirationAge
: 900 secs = 15 min.
var action = component.get("c.svrGetAccounts");
action.setStorable(); // here the magic happens # Step:6 in the animation below
// takes in opt parameter: ignoreExisting - default value = false
// set it true when you need to bypass the cache
// useful: when you know that cached-data is invalid you wnat to ignore it,
// so you do not use it on normal cases
action.setCallback(this, function(response) { // callback may be called twice as in sceneario:3 as shown below
// use the response
});
$A.enqueueAction(action);
Notes
A storable action might result in no call to the server. Never mark as storable an action that updates or deletes data.. Use it for non-mutable data (read-only). Since most server requests are read-only and idempotent, storable action will provide performance boost in these cases.
For storable actions in the cache, the framework returns the cached response immediately and also refreshes the data if it’s stale. Therefore, storable actions might have their callbacks invoked twice:
- first with cached data
- with updated data from the server when when
age >= refreshAge
(30 secs)
Keys used in determining an identical action:
- Apex controller name
- Method name (
@AuraEnabled
) - Method parameter values (method signature and parameter values)
Lightning Data Services (LDS): force:recordData
Provides well-connected Components
Component A asks LDS:
[Contact.id, Contact.Name, Contact.Phone]
- LDS gets that data from the Server and Caches it
Component B asks LDS:
[Contact.id, Contact.Name, Contact.Phone]
- LDS gets that data from the Cache
- Component C asks LDS:
[Contact.id, Contact.Name, Contact.Phone, Contact.MobilePhone]
- LDS gets that data from the Cache and fetches the delta - extra field
Contact.MobilePhone
and Caches it
- LDS gets that data from the Cache and fetches the delta - extra field
Component A Markup
<aura:component implements="force:hasRecordId, flexipage:availableForAllPageTypes" access="global">
<aura:attribute name="recordId" type="Id" />
<aura:attribute name="contact" type="MyContact__c" />
<force:recordData aura:id="dataService"
recordId="{!v.recordId}"
targetFields="{!v.contact}"
fields="['Id, 'Name', 'Phone__c']"
/>
</aura:component>
Component B Markup
<aura:component implements="force:hasRecordId, flexipage:availableForAllPageTypes" access="global">
<aura:attribute name="recordId" type="Id" />
<aura:attribute name="contact" type="MyContact__c" />
<force:recordData aura:id="dataService"
recordId="{!v.recordId}"
targetFields="{!v.contact}"
fields="['Id, 'Name', 'Phone__c']"
/>
<!-- other stuff -->
</aura:component>
Component C Markup
<aura:component implements="force:hasRecordId, flexipage:availableForAllPageTypes" access="global">
<aura:attribute name="recordId" type="Id" />
<aura:attribute name="contact" type="MyContact__c" />
<force:recordData aura:id="dataService"
recordId="{!v.recordId}"
targetFields="{!v.contact}"
fields="['Id, 'Name', 'Phone__c', 'Mobile_Phone__c']"
/>
<!-- other stuff -->
</aura:component>
Data Binding
Component with Bound Expression - 2-way data-binding
<aura:component controller="ContactCtrl" implements="force:hasRecordId, flexipage:availableForAllPageTypes"
access="global">
<aura:attribute name="contacts" type="Contact[]" />
<aura:iteration items="{!v.contacts}" var="contact">
<!-- Note: {!contact.firstName} : this is 2-way data-binding to var contact-->
<lightning:input value="{!contact.firstName}" />
<!--
- if user changes this value in the input element, the model (var contact) changes automatically
- At framework level we need to setup 2 event-listeners - this is in a loop here,
so 2 event-listeners per iterations...
- this is expensive!
// handle Model to View change
1. contact.addEventListener("change", function(event) {
// put into in VIEW - input element
inputElement.value = contact.firstName;
}
// handle View to Model change
2. inputElment.addEventListener("change", function(event) {
// update the model from value from the input element
contact.firstName = inputElement.value;
}
-->
</aura:iteration>
</aura:component>
Component with unBound Expression - 1-TIME data-binding
<aura:component controller="ContactCtrl" implements="force:hasRecordId, flexipage:availableForAllPageTypes"
access="global">
<aura:attribute name="contacts" type="Contact[]" />
<aura:iteration items="{!v.contacts}" var="contact">
<!-- Note: {#contact.firstName} : this is 1-TIME data-binding to var contact-->
<span> {#contact.firstName} </span>
<!--
- this is 1-TIME data-binding
- cheap!
-->
</aura:iteration>
</aura:component>
Note:
Don’t use a component’s init
event and client-side controller to initialize an attribute that is used in an unbound expression.
The attribute will not be initialized. Use a bound expression instead.
Modular Code
Share Code and Data Between Components
JS library
Write simple simple counter JS library
// simple Counter lib
// singleton
window.counter = (function() {
var value = 0;
return { // returns 2 functions
inc: function() {
return ++value;
},
dec: function() {
return --value;
},
getValue : function () {
return value;
}
}
} () );
// non-singleton
window.counter2 = function() {
var value = 0;
return {
inc : function() { return ++value; },
dec : function() { return --value; },
getValue: function () { return value; }
};
};
Counter Component using this Simple JS library
<aura:component implements="flexipage:availableForAllPageTypes"
access="global">
<!-- require this js lib -->
<ltng:require scripts="{!$Resource.gw19__Counter}" />
<!-- counter value -->
<aura:attribute name="value" type="Integer" default="0" />
<aura:attribute name="help" type="String" default="Help!" />
<!-- component intialization
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
-->
<div>
<div>Value: {!v.value}</div>
<lightning:button class='slds-button slds-button_outline-brand' onclick="{!c.getValue}"> Get Value </lightning:button>
<lightning:button class='slds-button slds-button_brand' onclick="{!c.inc}"> Increment Value </lightning:button>
<lightning:button class='slds-button slds-button_destructive' onclick="{!c.dec}"> Decrement Value </lightning:button>
<lightning:button class='slds-button slds-button_success' onclick="{!c.showHelp}"> Help </lightning:button>
<div> <hr/>{!v.help}</div>
</div>
</aura:component>
Component Controller
({
doInit : function(component, event, helper) {
component.set('v.value', 0);
},
getValue : function(component, event, helper) {
var value = window.counter.getValue();
component.set('v.value', value);
},
inc : function(component, event, helper) {
var value = window.counter.inc();
component.set('v.value', value);
},
dec : function(component, event, helper) {
var value = window.counter.dec();
component.set('v.value', value);
},
showHelp : function(component, event, helper) {
var help = "Here comes help content for this component...";
component.set('v.help', help);
},
})
Testing App
<aura:application extends="force:slds">
<gw19:Counter />
</aura:application>
Testing App CSS
.THIS {
padding:10px;
margin:30px;
border:5px solid steelblue;
border-radius:10px;
}
Demo with Aura App
Demo in Lightning Page built with App Builder
Counter2 Component using this Simple JS library
<aura:component implements="flexipage:availableForAllPageTypes"
access="global">
<!-- require this js lib and call jsLoaded to create the window.counter2 object
so window.counter2 object is unique to this component instance - not a singleton
-->
<ltng:require scripts="{!$Resource.gw19__Counter}"
afterScriptsLoaded="{!c.jsLoaded}" />
<!-- counter value -->
<aura:attribute name="value" type="Integer" default="0" />
<aura:attribute name="help" type="String" default="Help!" />
<!-- component intialization
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
-->
<div>
<div>Value: {!v.value}</div>
<lightning:button class='slds-button slds-button_outline-brand' onclick="{!c.getValue}"> Get Value </lightning:button>
<lightning:button class='slds-button slds-button_brand' onclick="{!c.inc}"> Increment Value </lightning:button>
<lightning:button class='slds-button slds-button_destructive' onclick="{!c.dec}"> Decrement Value </lightning:button>
<lightning:button class='slds-button slds-button_success' onclick="{!c.showHelp}"> Help </lightning:button>
<div> <hr/>{!v.help}</div>
</div>
</aura:component>
Component Controller
({
doInit : function(component, event, helper) {
component.set('v.value', 0);
},
jsLoaded: function(component, event, helper) {
// here create a new object for window.counter2 and
// store in component.counter object
component.counter = new window.counter2();
},
getValue : function(component, event, helper) {
var value = component.counter.getValue();
component.set('v.value', value);
},
inc : function(component, event, helper) {
var value = component.counter.inc();
component.set('v.value', value);
},
dec : function(component, event, helper) {
var value = component.counter.dec();
component.set('v.value', value);
},
showHelp : function(component, event, helper) {
var help = "Here comes help content for this component...";
component.set('v.help', help);
},
})
Demo in Lightning Page built with App Builder showing both singleton and non-singleton window.counter
Non-visual component - DataService components
Use <aura:method> to define a method as part of a component's API. This enables you to directly call a method in a **component’s client-side controller instead of firing and handling a component event**.
Using <aura:method> simplifies the code needed for a parent component to **call a method on a child component** that it contains.
**AccountDataService component - has no UI - non-visual**
```xml
<aura:component controller="AccountCtrl">
<!-- this findAll method another component can invoke -->
<aura:method name="findAll" action="{!c.getAccounts}">
<aura:attribute name="callback" type="function" />
</aura:method>
</aura:component>
Server-side controller AccountCtrl
public class AccountCtrl {
@AuraEnabled
public static List<Account> svrGetAllAccounts(){
List<Account> accounts = [SELECT Name FROM Account];
return accounts;
}
}
AccountDataService component controller
({
getAccounts : function (component, event, helper) {
var params = event.getParam("arguments");
var action = component.get("c.svrGetAllAccounts");
action.setStorable();
action.setCallback (this, function(response){
if (response.getState() === 'SUCCESS') {
params.callback(null, response.getReturnValue() );
} else {
params.callback(response.getError() );
}
});
$A.enqueueAction(action);
}
})
AccountList component using AccountDataService
<!-- has no controller but it uses AccountDataService
NOTE: Generally Component can have only one Service-side controller
This way a component can have make use many DataServices
-->
<!-- has no controller but it uses AccountDataService
NOTE: Generally Component can have only one Service-side controller
This way a component can have make use many DataServices
-->
<aura:component implements="flexipage:availableForAllPageTypes"
access="global">
<aura:attribute name="accounts" type="Account[]" />
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
<gw19:AccountDataService aura:id="ads" />
<!--
<gw19:ContactDataService aura:id="cds" />
-->
<!--
NOTE: Generally Component can have only one Service-side controller
This way a component can have make use many DataServices
-->
<ul>
<aura:iteration items="{!v.accounts}" var="account">
<li>{!account.Name}</li>
</aura:iteration>
</ul>
</aura:component>
Controller for AccountList Component
({
doInit : function (component, event, helper) {
var ads = component.find("ads"); // get the AccountDataService
// call findAll method on AccountDataService
ads.findAll($A.getCallback( function (error, data) {
component.set('v.accounts', data);
}));
}
})
TestApp
<aura:application extends="force:slds">
<gw19:Counter />
<gw19:Counter2 />
<gw19:AccountList />
</aura:application>
Events
- Application-Events are broadcast time : Costly
If Component-B is included in Component-A (have a parent-child relationship):
- Component-B was to say something to Component-A - Use Component-Event, not Application-Event
If Component-A and Component-B do not have a parent-child relationship:
- Component-B was to say something to Component-A - Use Application-Event
- This is typical App-Builder use case
If you using Application-level outside the App-Builder use-case, think twice and consider Component-Events
If two systems: System-A and System-B want to communicate each other, user Platform Events
System to System event - platform event and Component-Event Example
Subscriber component
<aura:component implements="flexipage:availableForAllPageTypes,flexipage:availableForRecordHome"
controller="SessionCtrl"
access="global">
<!--
SessionController provides svrGetSessionId()
-->
<aura:attribute name="eventName" type="String" />
<ltng:require scripts="{!$Resource.cometd + '/cometd.js' } "
afterScriptsLoaded="{!c.onJSLoaded}"
/>
<aura:registerEvent name="onmessage" type="gw19:MessageEvent" />
</aura:component>
Subscriber component controller
({
onJSLoaded: function(component, event, helper) {
var action = component.get("c.svrGetSessionId");
action.setCallback(this, function(response) {
// get sessionId
var sessionId = response.getReturnValue();
// get the cometd object
var cometd = new window.org.cometd.CometD();
// now configure the cometd object
cometd.configure({
url: window.location.protocol + '//' +
window.location.hostname +
'/cometd/41.0/',
requestHeader: { Authorization: 'OAuth ' + sessionId },
appendMessageTypeToURL : false
});
// comet handshake
cometd.handshake($A.getCalback (function (status){
if (status.successful) {
var eventName = component.get("v.eventName");
// eventName will Mix_Approved__e here...
cometd.subscribe('/event/' + eventName , $A.getCallback(
// get the message from the Platform Event (Mix_Approved__e)
// and fire the Component-event (onmessage)
function( message) {
console.log(message);
var messageEvent = component.getEvent("onmessage");
messageEvent.setParam("message", message);
//fire: component-event the MixPath component listens to this component-event
// after receiving this event it changes the step in the Path
messageEvent.fire();
}
)
);
}
}));
}); // action.setCallback...
$A.enqueueAction(action);
},
})
SessionCtrl - server-side controller
public class SessionCtrl {
@AuraEnabled
// exposes this method to Lightning component controller
public static String svrGetSessionId(){
// uses std UserInfo utility class to get sessionId
return UserInfo.getSessionId();
}
}
MixPath Component
<aura:component implements="flexipage:availableForAllPageTypes,flexipage:availableForRecordHome"
access="global">
<aura:attribute name="recordId" type="Id" />
<aura:attribute name="record" type="Object" />
<aura:attribute name="steps" type="String[]" default ="['Draft', 'Submitted to Mfr', 'Approved by Mfr', 'Ordered']" />
<!-- The Subscriber will translate this Platform event(Mix_Approved__e) to Component-Event (onMixApproved)
-->
<gw19:Subscriber eventName="Mix_Approved__e" onmessage="{!c.onMixApproved}" />
<force:recordData
aura:id="mixRecord"
recordId="{!v.recordId}"
targetFields="{!v.record}"
fields="['Id', 'Status__c']"
recordUpdated="{!c.onRecordUpdated}"
/>
<gw:Path steps="{!v.steps}" currentStep="{!v.record.Status__c}"
stepChange="{!c.onStepChange}"
/>
</aura:component>
Demo of using Platform Events and Component Events
- Subscribe to the event created by the Manufacturing event - Platform Event
- On the receipt that Platform Event, a component-event is fired