Maximizing Browser Performance with the Salesforce Console

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_minreportAs 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.

public class entityListController {
 // Note: the default list of items returned is 20 so we use 'setPageSize()' here to 
 // ensure we get all records returned. 
 
 // Get the list of Accounts for the Org
 public List<Account> fullAccList { get{ return getAccounts(); }}
 public List<Account> getAccounts() {
     ApexPages.StandardSetController allAccs = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Account LIMIT 500]));
     allAccs.setPageSize(500);
     return allAccs.getRecords();
 }
 
 public List<Account> fullBusAccList { get{ return getBusAccounts(); }}
     public List<Account> getBusAccounts() {
     ApexPages.StandardSetController allBusAccs = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Account WHERE isPersonAccount=false LIMIT 500]));
     allBusAccs.setPageSize(500);
     return allBusAccs.getRecords();
 }
 
 public List<Case> fullCaseList { get{ return getCases(); }}
 public List<Case> getCases() {
     ApexPages.StandardSetController allCases = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Case LIMIT 500]));
     allCases.setPageSize(500);
     return allCases.getRecords();
 }
 
 public List<Opportunity> fullOppList { get{ return getOpps(); }}
 public List<Opportunity> getOpps() {
     ApexPages.StandardSetController allOpps = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Opportunity LIMIT 500]));
     allOpps.setPageSize(500);
     return allOpps.getRecords();
 }
 
 public List<Lead> fullLeadList { get{ return getLeads(); }}
 public List<Lead> getLeads() {
     ApexPages.StandardSetController allLeads = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Lead LIMIT 500]));
     allLeads.setPageSize(500);
     return allLeads.getRecords();
 }
 
 public List<Contact> fullContactList { get{ return getContacts(); }}
 public List<Contact> getContacts() {
     ApexPages.StandardSetController allContacts = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Id FROM Contact LIMIT 500]));
     allContacts.setPageSize(500);
     return allContacts.getRecords();
 }
 
 public PageReference save(){
     return null;
 }

}

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.

<apex:page controller="entityListController">

    <apex:includeScript value="/support/console/37.0/integration.js"/>
    <script type="text/javascript">
    
    //* * * * * * * * * * * * * * * * * * * * * //
    //*************** Test Setup ***************//
    //* * * * * * * * * * * * * * * * * * * * * //        
        // Gets all 18-digit IDs of the entity type selected in the picklist
        // Input is the number of records needed at a minimum
        function getEntityIds(minRecordsNeeded) {
            var myPicklistElement = document.getElementById('entityPickList');
            var myPicklistValue = myPicklistElement.options[myPicklistElement.selectedIndex].value;
            
            var entityIdList = [];
            
            switch (myPicklistValue) {
                case 'Accounts':
                    entityIdList = makeArrayFromString('{!fullAccList}');
                    break;
                case 'Business Accounts':
                    entityIdList = makeArrayFromString('{!fullBusAccList}');
                    break;                    
                case 'Cases':
                    entityIdList = makeArrayFromString('{!fullCaseList}');
                    break;
                case 'Opportunities':
                    entityIdList = makeArrayFromString('{!fullOppList}');
                    break;
                case 'Leads':
                    entityIdList = makeArrayFromString('{!fullLeadList}');
                    break;
                case 'Contacts':
                    entityIdList = makeArrayFromString('{!fullContactList}');
                    break;
            }
            
            // Verify there are enough records to run the test
            if (entityIdList.length < minRecordsNeeded) {
                alert('Not enough records of type ' + myPicklistValue +  ' to run the test. Need '
                    + minRecordsNeeded + ' records but only have ' + entityIdList.length +
                    ' records.  Please select another entity type in the picklist.');
                return;
            }
            
            return getEntityIdDigits(entityIdList);            
        }
        
        // Get the appropriate ID lengths of an array of 18-digit IDs based on picklist
        function getEntityIdDigits(entityIdArray) {
            var myPicklistElement = document.getElementById('idLengthPickList');
            var entityIdLen = myPicklistElement.options[myPicklistElement.selectedIndex].value;
        
            var entityIdList = (entityIdLen == 'digit15') ? 
                make15DigitIdArray(entityIdArray) : entityIdArray;
            return entityIdList;
        }
        
        // ID list is returned as 1 long string; this turns
        // them back into arrays
        function makeArrayFromString(longString) {
            longString = longString.replace('[','');      // Remove "["
            longString = longString.replace(']','');      // Replace "]"
            longString = longString.replace(/\s/g,'');    // Remove whitespace
            return longString.split(",");
        }
        
        // Create 15-digit ID array from an 18-digit ID Array
        function make15DigitIdArray(orig18DigitArray) {
            var new15DigitArray = [];
            for (i = 0; i < orig18DigitArray.length; i++) { 
                 new15DigitArray[i] = orig18DigitArray[i].substring(0,15);
                 //alert(orig18DigitArray[i] + " to " + new15DigitArray[i]);
            }
            return new15DigitArray;
        }
    
    //* * * * * * * * * * * * * * * * * * * * * //
    //************* Test Functions *************//
    //* * * * * * * * * * * * * * * * * * * * * //    
        //*
        //* Baseline Test 01: Open & Close set of Primary Tabs
        //*   repeatedly
        //*
        //* 1. Opens 10 Primary Tabs
        //* 2. Waits 20 seconds
        //* 3. Closes all open tabs
        //* 4. Repeats steps 1-3 a total of 30 times
        //*
        function Baseline01OpenCloseTabs() {
            var numOfRuns = 30;          // Number of runs
            var runInterval = 20000;     // Interval time in milliseconds between runs
            var numOfTabs = 10;          // number of tabs to be opened in each loop
            
            openCloseTabsTest(numOfRuns, runInterval, numOfTabs, 'Baseline01', false);
        }
                
        //*
        //* Baseline Test 02: Open & Close 1 Primary Tabs
        //*   at a time; repeat 100 times
        //*
        //* 1. Opens 1 Primary Tab
        //* 2. Waits 10 seconds
        //* 3. Closes the open tab
        //* 4. Repeats steps 1-3 a total of 100 times
        //*
        function Baseline02OpenCloseTabs() {
        
            var numOfRuns = 100;          // Number of runs
            var runInterval = 10000;    // Interval time in milliseconds between runs
            var numOfTabs = 1;         // number of tabs to be opened in each loop
            
            openCloseTabsTest(numOfRuns, runInterval, numOfTabs, 'Baseline02', false);
        }
                
        //*
        //* Baseline Test 03: Open & Close 1 Sub Tab
        //*   at a time; repeat 100 times
        //*
        //* 1. Opens 1 Primary Tab
        //* 2. Opens 1 Sub Tab
        //* 3. Waits 10 seconds
        //* 4. Closes the open Sub Tab tab
        //* 5. Repeats steps 2-4 a total of 100 times
        //*
        function Baseline03OpenCloseSubTabs() {
        
            var numOfRuns = 100;          // Number of runs
            var runInterval = 10000;    // Interval time in milliseconds between runs
            var numOfTabs = 1;         // number of tabs to be opened in each loop
            
            openCloseTabsTest(numOfRuns, runInterval, numOfTabs, 'Baseline03', true);           
        }
                
        //*
        //* Baseline Test 04: Open 25 tabs at a
        //*   time and refreshes them all; repeat
        //*   10 times
        //*
        //* 1. Opens 25 Primary Tabs
        //* 2. Waits 30 seconds
        //* 3. Refreshes all Primary Tabs
        //* 4. Repeats steps 2-3 a total of 10 times
        //*
        function Baseline04OpenCloseSubTabs() {
        
            var numOfRuns = 10;          // Number of runs
            var runInterval = 30000;    // Interval time in milliseconds between runs
            var numOfTabs = 25;         // number of tabs to be opened in each loop
            
            refreshTabsTest(numOfRuns, runInterval, numOfTabs, 'BaseLine04', false);
        }
        

        
    //* * * * * * * * * * * * * * * * * * * * * //
    //************ Helper Functions ************//
    //* * * * * * * * * * * * * * * * * * * * * //
        
        //*
        //* Helper function that opens tabs, waits a specified time,
        //* closes all tabs, and then repeats for a specified loop count
        //*
        //* Inputs:
        //*    numOfRuns: The number of times to repeat the opening/closing of tabs
        //*    runInterval: The wait time between finishing opening tabs and closing them all
        //*    numOfTabs: The number of tabs to open at a time
        //*    testStatusId: ID of the element on the page where we will show how many runs are
        //*        left and indicate test completion
        //*    subTabTest: TRUE is the test is being conducted for subtabs; false if for primary tabs
        //*
        function openCloseTabsTest(numOfRuns, runInterval, numOfTabs, testName, subTabTest) {
            var loopCount = 0;
            var runsLeft = numOfRuns;
            var tabIds = [];
            var entityIds = getEntityIds(numOfTabs);
            var primTabId = '0';

            // Fill in the Test Results section with the test name and clear any old data
            document.getElementById("TestName").innerHTML = testName;
            document.getElementById("TestProgress").innerHTML = "";
            
            if (subTabTest == true) {
                // Open a Primary Tab that is not the same entity type as what
                // we will open as subtabs to ensure we will not be opening a duplicate
                var myPicklistElement = document.getElementById('entityPickList');
                var myPicklistValue = myPicklistElement.options[myPicklistElement.selectedIndex].value;
                var primTabEntityId = (myPicklistValue == 'Accounts') ? makeArrayFromString('{!fullContactList}')[0] : makeArrayFromString('{!fullAccList}')[0];
                
                sforce.console.openPrimaryTab(null, "/" + primTabEntityId, true,
                    primTabEntityId, getPrimTabId, primTabEntityId);
                
            } else {
                openCloseTabs();
            }
            
            // Opens and Closes Primary Tabs
            function openCloseTabs() {
                var i;
                var offset = loopCount*numOfTabs; // offset
                for (i = 0; i < numOfTabs; i++) {
                    var idx = (offset + i) % entityIds.length;
                    sforce.console.openPrimaryTab(null, "/" + entityIds[idx],
                        true, entityIds[idx], callback, entityIds[idx]);
                }
            }
            
            // Opens a Primary Tab for Subtab tests
            function getPrimTabId(result) {
                sforce.console.getFocusedPrimaryTabId(startSubTabTest);
            }
            
            // Sets up for opening Subtabs under the opened
            // Primary Tabs
            function startSubTabTest(result) {
                primTabId = result.id;
                openCloseSubTabs();
            }
            
            // Opens and Closes Sub Tabs
            function openCloseSubTabs() {    
                var i;
                var offset = loopCount*numOfTabs; // offset
                for (i = 0; i < numOfTabs; i++) {
                    var idx = (offset + i) % entityIds.length;
                    sforce.console.openSubtab(primTabId, "/" + entityIds[idx],
                        true, entityIds[idx], null, callback, entityIds[idx]);
                }
            }
            
            // Callback for cycling the tests and waiting between runs
            function callback(result) {
                tabIds.push(result.id);
                if (tabIds.length === numOfTabs) {
                    runsLeft--;
                    document.getElementById("TestProgress").innerHTML =
                        "runs left: " + runsLeft;
        
                    setTimeout(function() {
                        loopCount++;
                        var i;
                        for (i = 0; i < tabIds.length; i++) {
                            sforce.console.closeTab(tabIds[i]);
                        }
                                        
                        tabIds = [];
                        if (runsLeft > 0) {
                            if (subTabTest == true) {
                                openCloseSubTabs(primTabId);
                            } else {
                                openCloseTabs();
                            }
                        } else {
                            if (subTabTest == true) sforce.console.closeTab(primTabId);
                            runsLeft = numOfRuns;
                            document.getElementById("TestProgress").innerHTML = "Test Completed";
                        }
                    }, runInterval );
                }
            }
        }
                
        //*
        //* Helper function that opens tabs, waits a specified time,
        //* refreshes them, and then repeats for a specified loop count
        //*
        //* Inputs:
        //*    numOfRuns: The number of times to repeat the tabs refreshes
        //*    runInterval: The wait time between refreshes
        //*    numOfTabs: The number of tabs to open at a time
        //*    testStatusId: ID of the element on the page where we will show how many runs are
        //*        left and indicate test completion
        //*    subTabTest: TRUE is the test is being conducted for subtabs; false if for primary tabs
        //*
        function refreshTabsTest(numOfRuns, runInterval, numOfTabs, testName, subTabTest) {
            numOfRuns++;  // Add 1 since the initial run is to open the tabs
            var runsLeft = numOfRuns;
            var tabIds = [];
            var entityIds = getEntityIds(numOfTabs);
            var primTabId;
            var tabsRefreshed = numOfTabs;
            
            // Fill in the Test Results section with the test name and clear any old data
            document.getElementById("TestName").innerHTML = testName;
            document.getElementById("TestProgress").innerHTML = "";
            
            if (subTabTest == true) {
                // Open a Primary Tab that is not the same entity type as what
                // we will open as subtabs to ensure we will not be opening a duplicate
                var myPicklistElement = document.getElementById('entityPickList');
                var myPicklistValue = myPicklistElement.options[myPicklistElement.selectedIndex].value;
                var primTabEntityId = (myPicklistValue == 'Accounts') ? makeArrayFromString('{!fullContactList}')[0] : makeArrayFromString('{!fullAccList}')[0];
                
                sforce.console.openPrimaryTab(null, "/" + primTabEntityId, true,
                    primTabEntityId, openSubTabs, primTabEntityId);
                
            } else {
                openPrimaryTabsForRefresh();
            }
            
            function openSubTabs(result) {
                // To be added for any Sub Tab refresh tests
            }
            
            function openPrimaryTabsForRefresh() {
                var i;
                for (i = 0; i < numOfTabs; i++) {
                    sforce.console.openPrimaryTab(null, "/" + entityIds[i],
                        true, entityIds[i], callback, entityIds[i]);
                }
            }
            
                
            // Callback for cycling the tests and waiting between runs
            function callback(result) {
                tabIds.push(result.id);
                if (tabIds.length === numOfTabs) {
                    refTabCycle();
                }
            }
            
            function refrPrimTabs() {
                var i;
                for (i = 0; i < numOfTabs; i++) {
                    sforce.console.refreshPrimaryTabById(tabIds[i], false, refTabCycle);
                }
            }
            
            function refTabCycle() {
                if (tabsRefreshed == numOfTabs) {
                    tabsRefreshed = 0;
                    runsLeft--;
                    document.getElementById("TestProgress").innerHTML =
                            "runs left: " + runsLeft;
                    
                    setTimeout(function() {

                        if (runsLeft > 0) {
                            if (subTabTest == true) {
                                //add when/if SubTab test is activated
                            } else {
                                refrPrimTabs();
                            }
                        } else {
                            var i;
                            for (i = 0; i < tabIds.length; i++) {
                                sforce.console.closeTab(tabIds[i]);
                            }
                            
                            if (subTabTest == true) sforce.console.closeTab(primTabId);
                            document.getElementById("TestProgress").innerHTML = "Test Completed. Cleaned a total of " + top.Sfdc._counter + " tabs.";
                        }
                    }, runInterval );
                }
                tabsRefreshed++;
            }
        }
               
    </script>
    
    <p><center><h1>Perf Baseline tests</h1></center></p>
    
    <apex:form rendered="true">
        <!-- Picklists for User to specify Entity type and ID length -->
    <p><h2>Select Entity and ID Length</h2><br/>
        <select id="entityPickList">
            <option value="Accounts">Accounts</option>
            <option value="Business Accounts">Business Accounts</option>
            <option value="Cases">Cases</option>
            <option value="Opportunities">Opportunities</option>
            <option value="Leads">Leads</option>
            <option value="Contacts">Contacts</option>
        </select>
        
        <select id="idLengthPickList">
            <option value="digit15">15-digits</option>
            <option value="digit18">18-digits</option>
        </select></p>
    </apex:form>
           
    <!-- Baseline 01 -->
    <p><h2>Baseline 01:</h2>
    Open 10 tabs, wait 20 sec, close tabs, repeat 30x<br/>
    <apex:form >
        <apex:commandButton value="Run Baseline01" action="{!save}" onclick="Baseline01OpenCloseTabs();" rerender="out"/>
    </apex:form>
    </p>    
    <!-- Baseline 02 -->
    <p><h2>Baseline 02:</h2>
    Open 1 tab, wait 10 sec, close tab, repeat 100x<br/>
    <apex:form >
        <apex:commandButton value="Run Baseline02" action="{!save}" onclick="Baseline02OpenCloseTabs();" rerender="out"/>
    </apex:form>
    </p>    
    <!-- Baseline 03 -->
    <p><h2>Baseline 03:</h2>
    Open 1 Primary tab then open 1 Subtab, wait 10 sec, close Sub Tab, repeat 100x<br/>
    <apex:form >
        <apex:commandButton value="Run Baseline03" action="{!save}" onclick="Baseline03OpenCloseSubTabs();" rerender="out"/>
    </apex:form>
    </p>    
    <!-- Baseline 04 -->
    <p><h2>Baseline 04:</h2>
    Open 25 Primary tabs, waits 30 seconds, refreshes all tabs and repeats refresh action 10x<br/>
    <apex:form >
        <apex:commandButton value="Run Baseline04" action="{!save}" onclick="Baseline04OpenCloseSubTabs();" rerender="out"/>
    </apex:form>
    </p>
        
    <!-- Results Display -->
    <p>----------------------------------------<br/>
    <h2>Test Results</h2>
    <b><i><div id="TestName"></div></i></b>
    <div id="TestProgress"></div>
    </p>    
</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

tagged , , Bookmark the permalink. Trackbacks are closed, but you can post a comment.