Salesforce Developers Blog

Maximizing Browser Performance with the Salesforce Console

Avatar for Paul HigginsPaul Higgins
This article is about the Salesforce Console for service and sales. It does not refer to the Developer Console, or the new Lightning Console Apps coming in Spring ’17. As we mentioned in an earlier blog post here, there can be a significant difference in Console performance depending on which browser you choose.
Maximizing Browser Performance with the Salesforce Console
January 05, 2017
Listen to this article
0:00 / 0:00

This article is about the Salesforce Console for service and sales. It does not refer to the Developer Console, or the new Lightning Console Apps coming in Spring ’17.

advanced_console_minreport As we mentioned in an earlier blog post here, there can be a significant difference in Console performance depending on which browser you choose. This article will give you some implementation best practices to avoid performance degradation and provide code that can be used to systematically stress test and measure the impact of your customizations.

Disclaimer: The code presented works at the time of publication. Although our API is backwards compatible this post makes no guarantee of the functionality in the future or any compatibility with potential future console products or APIs. It is best used as a reference for your own implementation.

Implementation Tips

There are a few considerations when designing a console implementation that can help reduce or avoid performance problems entirely. The Salesforce Console makes heavy use of iframes, whose performance varies between browsers. For all implementations we advise thinking about ways to reduce the number of active iframes as much as possible.

Tab governance settings

The console has built-in functionality which can prevent users from opening too many tabs at once. Just like a regular browser, performance may suffer if you have 50 tabs open at once rather than a healthier lower number. Enabling tab limits will keep users from leaving un-necessary tabs open. Details on how to set this up can be found here. Our recommendation is a limit of 20 primary tabs and 10 subtabs.

Use visualforce components to reduce iframes

If you are using multiple visualforce pages as console sidebars or embedded into a page layout, consider if their functionality can be combined. The console will load each visualforce page in its own iframe. Combining them into a single page results in less frames and better performance. By creating multiple visualforce custom components to build a single visualforce page you can reduce the overhead compared to using multiple visualforce pages individually. You can learn more about visualforce custom components here.

Avoid opening multiple tabs automatically

As an example, let’s say you have a visualforce page which gets opened when a call comes in from your CTI adapter. This VF page also opens 4 subtabs when it loads: an account, contact, case, and lead. This means every call that comes in will open 5 tabs in your console, even if your agent does not necessarily need them all. In a high volume call center this can rapidly add up.

A better option would be to open a VF page which has one or more buttons that open each of those other kinds of tabs when clicked.

This is of course a balancing act between optimizing your agents’ workflow by shortening click paths and the performance impacts of doing so. If you’re wondering how to determine what impact such a change has in order to make a data driven decision, you’ll love the next section.

Testing the Performance of your Implementation

As mentioned in our earlier blog post, Salesforce runs nightly performance tests to evaluate the memory profile of certain actions. We’re happy to provide a portion of this code in order to help you do your own rigorous testing in a way which aligns with our own. This can help you determine the impact of each specific customization as you put it in place, allowing for better performing implementations.

Throughout the rest of the article we’ll create a custom console component which can open console tabs automatically in order to simulate a high volume support center.

Step 1: Create an apex class

This code is simply retrieving the records whose tab will be opened during the test. If you need to work with other entities, custom objects, or with a specific subset (based on record type, created date, or any other filter) you can simply create additional methods to select those records.

1public class entityListController {
2 // Note: the default list of items returned is 20 so we use 'setPageSize()' here to 
3 // ensure we get all records returned. 
4 
5 // Get the list of Accounts for the Org
6 public List<Account> fullAccList { get{ return getAccounts(); }}
7 public List<Account> getAccounts() {
8     ApexPages.StandardSetController allAccs = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Account LIMIT 500]));
9     allAccs.setPageSize(500);
10     return allAccs.getRecords();
11 }
12 
13 public List<Account> fullBusAccList { get{ return getBusAccounts(); }}
14     public List<Account> getBusAccounts() {
15     ApexPages.StandardSetController allBusAccs = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Account WHERE isPersonAccount=false LIMIT 500]));
16     allBusAccs.setPageSize(500);
17     return allBusAccs.getRecords();
18 }
19 
20 public List<Case> fullCaseList { get{ return getCases(); }}
21 public List<Case> getCases() {
22     ApexPages.StandardSetController allCases = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Case LIMIT 500]));
23     allCases.setPageSize(500);
24     return allCases.getRecords();
25 }
26 
27 public List<Opportunity> fullOppList { get{ return getOpps(); }}
28 public List<Opportunity> getOpps() {
29     ApexPages.StandardSetController allOpps = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Opportunity LIMIT 500]));
30     allOpps.setPageSize(500);
31     return allOpps.getRecords();
32 }
33 
34 public List<Lead> fullLeadList { get{ return getLeads(); }}
35 public List<Lead> getLeads() {
36     ApexPages.StandardSetController allLeads = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Lead LIMIT 500]));
37     allLeads.setPageSize(500);
38     return allLeads.getRecords();
39 }
40 
41 public List<Contact> fullContactList { get{ return getContacts(); }}
42 public List<Contact> getContacts() {
43     ApexPages.StandardSetController allContacts = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Contact LIMIT 500]));
44     allContacts.setPageSize(500);
45     return allContacts.getRecords();
46 }
47 
48 public PageReference save(){
49     return null;
50 }
51
52}

Step 2: Create a visualforce page.

This page lets you choose which type of object to work with and which test to run. You can extend or modify the javascript in order to run different tests, as well as update the picklists if you added new entity types into the controller.

The four tests which are included in this page are:

  1. Open 10 primary tabs at once, wait for them to load, and close them. Repeats this 30 times. This helps simulate behavior like receiving multiple chats which open related records at the same time.
  2. Open 1 primary tab then closes it. Repeats this 100 times.
  3. Opens 1 primary tab and 1 subtab. Closes the subtab. Repeats opening and closing subtabs 100 times.
  4. Opens 25 primary tabs at once. Refreshes them all at once. Repeats refresh 10 times.

It’s a simple matter to provide different parameters to the above tests, allowing you to open a simple tab 1000 times for example. Find methods with names like ‘Baseline01OpenCloseTabs’ and edit the variables that contain these values.

1<apex:page controller="entityListController">
2
3    <apex:includeScript value="/support/console/37.0/integration.js"/>
4    <script type="text/javascript">
5    
6    //* * * * * * * * * * * * * * * * * * * * * //
7    //*************** Test Setup ***************//
8    //* * * * * * * * * * * * * * * * * * * * * //        
9        // Gets all 18-digit IDs of the entity type selected in the picklist
10        // Input is the number of records needed at a minimum
11        function getEntityIds(minRecordsNeeded) {
12            var myPicklistElement = document.getElementById('entityPickList');
13            var myPicklistValue = myPicklistElement.options[myPicklistElement.selectedIndex].value;
14            
15            var entityIdList = [];
16            
17            switch (myPicklistValue) {
18                case 'Accounts':
19                    entityIdList = makeArrayFromString('{!fullAccList}');
20                    break;
21                case 'Business Accounts':
22                    entityIdList = makeArrayFromString('{!fullBusAccList}');
23                    break;                    
24                case 'Cases':
25                    entityIdList = makeArrayFromString('{!fullCaseList}');
26                    break;
27                case 'Opportunities':
28                    entityIdList = makeArrayFromString('{!fullOppList}');
29                    break;
30                case 'Leads':
31                    entityIdList = makeArrayFromString('{!fullLeadList}');
32                    break;
33                case 'Contacts':
34                    entityIdList = makeArrayFromString('{!fullContactList}');
35                    break;
36            }
37            
38            // Verify there are enough records to run the test
39            if (entityIdList.length < minRecordsNeeded) {
40                alert('Not enough records of type ' + myPicklistValue +  ' to run the test. Need '
41                    + minRecordsNeeded + ' records but only have ' + entityIdList.length +
42                    ' records.  Please select another entity type in the picklist.');
43                return;
44            }
45            
46            return getEntityIdDigits(entityIdList);            
47        }
48        
49        // Get the appropriate ID lengths of an array of 18-digit IDs based on picklist
50        function getEntityIdDigits(entityIdArray) {
51            var myPicklistElement = document.getElementById('idLengthPickList');
52            var entityIdLen = myPicklistElement.options[myPicklistElement.selectedIndex].value;
53        
54            var entityIdList = (entityIdLen == 'digit15') ? 
55                make15DigitIdArray(entityIdArray) : entityIdArray;
56            return entityIdList;
57        }
58        
59        // ID list is returned as 1 long string; this turns
60        // them back into arrays
61        function makeArrayFromString(longString) {
62            longString = longString.replace('[','');      // Remove "["
63            longString = longString.replace(']','');      // Replace "]"
64            longString = longString.replace(/s/g,'');    // Remove whitespace
65            return longString.split(",");
66        }
67        
68        // Create 15-digit ID array from an 18-digit ID Array
69        function make15DigitIdArray(orig18DigitArray) {
70            var new15DigitArray = [];
71            for (i = 0; i < orig18DigitArray.length; i++) { 
72                 new15DigitArray[i] = orig18DigitArray[i].substring(0,15);
73                 //alert(orig18DigitArray[i] + " to " + new15DigitArray[i]);
74            }
75            return new15DigitArray;
76        }
77    
78    //* * * * * * * * * * * * * * * * * * * * * //
79    //************* Test Functions *************//
80    //* * * * * * * * * * * * * * * * * * * * * //    
81        //*
82        //* Baseline Test 01: Open & Close set of Primary Tabs
83        //*   repeatedly
84        //*
85        //* 1. Opens 10 Primary Tabs
86        //* 2. Waits 20 seconds
87        //* 3. Closes all open tabs
88        //* 4. Repeats steps 1-3 a total of 30 times
89        //*
90        function Baseline01OpenCloseTabs() {
91            var numOfRuns = 30;          // Number of runs
92            var runInterval = 20000;     // Interval time in milliseconds between runs
93            var numOfTabs = 10;          // number of tabs to be opened in each loop
94            
95            openCloseTabsTest(numOfRuns, runInterval, numOfTabs, 'Baseline01', false);
96        }
97                
98        //*
99        //* Baseline Test 02: Open & Close 1 Primary Tabs
100        //*   at a time; repeat 100 times
101        //*
102        //* 1. Opens 1 Primary Tab
103        //* 2. Waits 10 seconds
104        //* 3. Closes the open tab
105        //* 4. Repeats steps 1-3 a total of 100 times
106        //*
107        function Baseline02OpenCloseTabs() {
108        
109            var numOfRuns = 100;          // Number of runs
110            var runInterval = 10000;    // Interval time in milliseconds between runs
111            var numOfTabs = 1;         // number of tabs to be opened in each loop
112            
113            openCloseTabsTest(numOfRuns, runInterval, numOfTabs, 'Baseline02', false);
114        }
115                
116        //*
117        //* Baseline Test 03: Open & Close 1 Sub Tab
118        //*   at a time; repeat 100 times
119        //*
120        //* 1. Opens 1 Primary Tab
121        //* 2. Opens 1 Sub Tab
122        //* 3. Waits 10 seconds
123        //* 4. Closes the open Sub Tab tab
124        //* 5. Repeats steps 2-4 a total of 100 times
125        //*
126        function Baseline03OpenCloseSubTabs() {
127        
128            var numOfRuns = 100;          // Number of runs
129            var runInterval = 10000;    // Interval time in milliseconds between runs
130            var numOfTabs = 1;         // number of tabs to be opened in each loop
131            
132            openCloseTabsTest(numOfRuns, runInterval, numOfTabs, 'Baseline03', true);           
133        }
134                
135        //*
136        //* Baseline Test 04: Open 25 tabs at a
137        //*   time and refreshes them all; repeat
138        //*   10 times
139        //*
140        //* 1. Opens 25 Primary Tabs
141        //* 2. Waits 30 seconds
142        //* 3. Refreshes all Primary Tabs
143        //* 4. Repeats steps 2-3 a total of 10 times
144        //*
145        function Baseline04OpenCloseSubTabs() {
146        
147            var numOfRuns = 10;          // Number of runs
148            var runInterval = 30000;    // Interval time in milliseconds between runs
149            var numOfTabs = 25;         // number of tabs to be opened in each loop
150            
151            refreshTabsTest(numOfRuns, runInterval, numOfTabs, 'BaseLine04', false);
152        }
153        
154
155        
156    //* * * * * * * * * * * * * * * * * * * * * //
157    //************ Helper Functions ************//
158    //* * * * * * * * * * * * * * * * * * * * * //
159        
160        //*
161        //* Helper function that opens tabs, waits a specified time,
162        //* closes all tabs, and then repeats for a specified loop count
163        //*
164        //* Inputs:
165        //*    numOfRuns: The number of times to repeat the opening/closing of tabs
166        //*    runInterval: The wait time between finishing opening tabs and closing them all
167        //*    numOfTabs: The number of tabs to open at a time
168        //*    testStatusId: ID of the element on the page where we will show how many runs are
169        //*        left and indicate test completion
170        //*    subTabTest: TRUE is the test is being conducted for subtabs; false if for primary tabs
171        //*
172        function openCloseTabsTest(numOfRuns, runInterval, numOfTabs, testName, subTabTest) {
173            var loopCount = 0;
174            var runsLeft = numOfRuns;
175            var tabIds = [];
176            var entityIds = getEntityIds(numOfTabs);
177            var primTabId = '0';
178
179            // Fill in the Test Results section with the test name and clear any old data
180            document.getElementById("TestName").innerHTML = testName;
181            document.getElementById("TestProgress").innerHTML = "";
182            
183            if (subTabTest == true) {
184                // Open a Primary Tab that is not the same entity type as what
185                // we will open as subtabs to ensure we will not be opening a duplicate
186                var myPicklistElement = document.getElementById('entityPickList');
187                var myPicklistValue = myPicklistElement.options[myPicklistElement.selectedIndex].value;
188                var primTabEntityId = (myPicklistValue == 'Accounts') ? makeArrayFromString('{!fullContactList}')[0] : makeArrayFromString('{!fullAccList}')[0];
189                
190                sforce.console.openPrimaryTab(null, "/" + primTabEntityId, true,
191                    primTabEntityId, getPrimTabId, primTabEntityId);
192                
193            } else {
194                openCloseTabs();
195            }
196            
197            // Opens and Closes Primary Tabs
198            function openCloseTabs() {
199                var i;
200                var offset = loopCount*numOfTabs; // offset
201                for (i = 0; i < numOfTabs; i++) {
202                    var idx = (offset + i) % entityIds.length;
203                    sforce.console.openPrimaryTab(null, "/" + entityIds[idx],
204                        true, entityIds[idx], callback, entityIds[idx]);
205                }
206            }
207            
208            // Opens a Primary Tab for Subtab tests
209            function getPrimTabId(result) {
210                sforce.console.getFocusedPrimaryTabId(startSubTabTest);
211            }
212            
213            // Sets up for opening Subtabs under the opened
214            // Primary Tabs
215            function startSubTabTest(result) {
216                primTabId = result.id;
217                openCloseSubTabs();
218            }
219            
220            // Opens and Closes Sub Tabs
221            function openCloseSubTabs() {    
222                var i;
223                var offset = loopCount*numOfTabs; // offset
224                for (i = 0; i < numOfTabs; i++) {
225                    var idx = (offset + i) % entityIds.length;
226                    sforce.console.openSubtab(primTabId, "/" + entityIds[idx],
227                        true, entityIds[idx], null, callback, entityIds[idx]);
228                }
229            }
230            
231            // Callback for cycling the tests and waiting between runs
232            function callback(result) {
233                tabIds.push(result.id);
234                if (tabIds.length === numOfTabs) {
235                    runsLeft--;
236                    document.getElementById("TestProgress").innerHTML =
237                        "runs left: " + runsLeft;
238        
239                    setTimeout(function() {
240                        loopCount++;
241                        var i;
242                        for (i = 0; i < tabIds.length; i++) {
243                            sforce.console.closeTab(tabIds[i]);
244                        }
245                                        
246                        tabIds = [];
247                        if (runsLeft > 0) {
248                            if (subTabTest == true) {
249                                openCloseSubTabs(primTabId);
250                            } else {
251                                openCloseTabs();
252                            }
253                        } else {
254                            if (subTabTest == true) sforce.console.closeTab(primTabId);
255                            runsLeft = numOfRuns;
256                            document.getElementById("TestProgress").innerHTML = "Test Completed";
257                        }
258                    }, runInterval );
259                }
260            }
261        }
262                
263        //*
264        //* Helper function that opens tabs, waits a specified time,
265        //* refreshes them, and then repeats for a specified loop count
266        //*
267        //* Inputs:
268        //*    numOfRuns: The number of times to repeat the tabs refreshes
269        //*    runInterval: The wait time between refreshes
270        //*    numOfTabs: The number of tabs to open at a time
271        //*    testStatusId: ID of the element on the page where we will show how many runs are
272        //*        left and indicate test completion
273        //*    subTabTest: TRUE is the test is being conducted for subtabs; false if for primary tabs
274        //*
275        function refreshTabsTest(numOfRuns, runInterval, numOfTabs, testName, subTabTest) {
276            numOfRuns++;  // Add 1 since the initial run is to open the tabs
277            var runsLeft = numOfRuns;
278            var tabIds = [];
279            var entityIds = getEntityIds(numOfTabs);
280            var primTabId;
281            var tabsRefreshed = numOfTabs;
282            
283            // Fill in the Test Results section with the test name and clear any old data
284            document.getElementById("TestName").innerHTML = testName;
285            document.getElementById("TestProgress").innerHTML = "";
286            
287            if (subTabTest == true) {
288                // Open a Primary Tab that is not the same entity type as what
289                // we will open as subtabs to ensure we will not be opening a duplicate
290                var myPicklistElement = document.getElementById('entityPickList');
291                var myPicklistValue = myPicklistElement.options[myPicklistElement.selectedIndex].value;
292                var primTabEntityId = (myPicklistValue == 'Accounts') ? makeArrayFromString('{!fullContactList}')[0] : makeArrayFromString('{!fullAccList}')[0];
293                
294                sforce.console.openPrimaryTab(null, "/" + primTabEntityId, true,
295                    primTabEntityId, openSubTabs, primTabEntityId);
296                
297            } else {
298                openPrimaryTabsForRefresh();
299            }
300            
301            function openSubTabs(result) {
302                // To be added for any Sub Tab refresh tests
303            }
304            
305            function openPrimaryTabsForRefresh() {
306                var i;
307                for (i = 0; i < numOfTabs; i++) {
308                    sforce.console.openPrimaryTab(null, "/" + entityIds[i],
309                        true, entityIds[i], callback, entityIds[i]);
310                }
311            }
312            
313                
314            // Callback for cycling the tests and waiting between runs
315            function callback(result) {
316                tabIds.push(result.id);
317                if (tabIds.length === numOfTabs) {
318                    refTabCycle();
319                }
320            }
321            
322            function refrPrimTabs() {
323                var i;
324                for (i = 0; i < numOfTabs; i++) {
325                    sforce.console.refreshPrimaryTabById(tabIds[i], false, refTabCycle);
326                }
327            }
328            
329            function refTabCycle() {
330                if (tabsRefreshed == numOfTabs) {
331                    tabsRefreshed = 0;
332                    runsLeft--;
333                    document.getElementById("TestProgress").innerHTML =
334                            "runs left: " + runsLeft;
335                    
336                    setTimeout(function() {
337
338                        if (runsLeft > 0) {
339                            if (subTabTest == true) {
340                                //add when/if SubTab test is activated
341                            } else {
342                                refrPrimTabs();
343                            }
344                        } else {
345                            var i;
346                            for (i = 0; i < tabIds.length; i++) {
347                                sforce.console.closeTab(tabIds[i]);
348                            }
349                            
350                            if (subTabTest == true) sforce.console.closeTab(primTabId);
351                            document.getElementById("TestProgress").innerHTML = "Test Completed. Cleaned a total of " + top.Sfdc._counter + " tabs.";
352                        }
353                    }, runInterval );
354                }
355                tabsRefreshed++;
356            }
357        }
358               
359    </script>
360    
361    <p><center><h1>Perf Baseline tests</h1></center></p>
362    
363    <apex:form rendered="true">
364        <!-- Picklists for User to specify Entity type and ID length -->
365    <p><h2>Select Entity and ID Length</h2><br/>
366        <select id="entityPickList">
367            <option value="Accounts">Accounts</option>
368            <option value="Business Accounts">Business Accounts</option>
369            <option value="Cases">Cases</option>
370            <option value="Opportunities">Opportunities</option>
371            <option value="Leads">Leads</option>
372            <option value="Contacts">Contacts</option>
373        </select>
374        
375        <select id="idLengthPickList">
376            <option value="digit15">15-digits</option>
377            <option value="digit18">18-digits</option>
378        </select></p>
379    </apex:form>
380           
381    <!-- Baseline 01 -->
382    <p><h2>Baseline 01:</h2>
383    Open 10 tabs, wait 20 sec, close tabs, repeat 30x<br/>
384    <apex:form >
385        <apex:commandButton value="Run Baseline01" action="{!save}" onclick="Baseline01OpenCloseTabs();" rerender="out"/>
386    </apex:form>
387    </p>    
388    <!-- Baseline 02 -->
389    <p><h2>Baseline 02:</h2>
390    Open 1 tab, wait 10 sec, close tab, repeat 100x<br/>
391    <apex:form >
392        <apex:commandButton value="Run Baseline02" action="{!save}" onclick="Baseline02OpenCloseTabs();" rerender="out"/>
393    </apex:form>
394    </p>    
395    <!-- Baseline 03 -->
396    <p><h2>Baseline 03:</h2>
397    Open 1 Primary tab then open 1 Subtab, wait 10 sec, close Sub Tab, repeat 100x<br/>
398    <apex:form >
399        <apex:commandButton value="Run Baseline03" action="{!save}" onclick="Baseline03OpenCloseSubTabs();" rerender="out"/>
400    </apex:form>
401    </p>    
402    <!-- Baseline 04 -->
403    <p><h2>Baseline 04:</h2>
404    Open 25 Primary tabs, waits 30 seconds, refreshes all tabs and repeats refresh action 10x<br/>
405    <apex:form >
406        <apex:commandButton value="Run Baseline04" action="{!save}" onclick="Baseline04OpenCloseSubTabs();" rerender="out"/>
407    </apex:form>
408    </p>
409        
410    <!-- Results Display -->
411    <p>----------------------------------------<br/>
412    <h2>Test Results</h2>
413    <b><i><div id="TestName"></div></i></b>
414    <div id="TestProgress"></div>
415    </p>    
416</apex:page>

Step 3: Create a console component

  1. From Setup, click Customize | Console | Custom Console Components and then click New.
    Give the custom component a name and a button name
  2. Under the Component option select Visualforce Page and enter the name of the page you created in step 2
  3. From Setup, click Build | Create | Apps and edit your console app. Under ‘Choose Console Components’ move your new component into the Selected Items list and save your changes

Step 4: Run the test and observe the results.

You’re now ready to begin. Enter your console app and open your new component. Select the object to test from the picklist and press the button for the test you want to run. Use a tool of your choice to measure the behavior of your browser’s memory over time, as well as comparing the values at the start and end of the test.

We recommend the use of Performance Monitor in Windows to monitor the private memory usage of your browser process. More information about using this tool can be found here.

After you have established a baseline with the tests, you can make your implementation changes and run the tests again to compare the results. If the changes you make result in excessive memory consumption compared to the baseline, it’s time to consider ways to optimize your implementation. You can also compare the behavior to the baselines established by Salesforce in our blog for a default console to confirm if the performance deviates from standard.

That’s it! Hopefully arming yourself these implementation and instrumentation tips will help your console reach new heights of performance and user satisfaction.

The future of Console

Starting in Spring ’17 the console is getting the full Lightning treatment. All-new Console Apps built natively on our Lightning platform will be available, including full support for Lightning components. Best of all, this new console taps into the performance and optimization that’s done across the entire Lightning Experience. This means you’ll get the same great performance inside the console as out of it.

Be sure to check out the release notes and roadmap for more information on the latest and greatest console features.

References:

https://developer.salesforce.com/blogs/developer-relations/2016/06/salesforce-console-performance-internet-explorer-firefox-chrome.html

https://msdn.microsoft.com/en-us/library/windows/hardware/ff560134(v=vs.85).aspx

https://releasenotes.docs.salesforce.com/en-us/spring16/release-notes/rn_console_tab_limit.htm

https://developer.salesforce.com/docs/atlas.en-us.pages.meta/pages/pages_comp_cust_def.htm

https://developer.salesforce.com/docs/atlas.en-us.api_console.meta/api_console/sforce_api_console_methods_tabs.htm

https://resources.docs.salesforce.com/206/latest/en-us/sfdc/pdf/salesforce_spring17_release_notes.pdf

www.salesforce.com/campaigns/lightning/#Roadmap