Codemky
Codemky

Reputation: 49

How can I further reduce load times (Visualforce, Apex, Angular)

I built a Visualforce page that pulls both Contacts and Leads into one view. It works great except it takes 20-25 seconds to load, which as we all know, is HORRIBLE for usability.

I've looked at the logs and it looks like Apex is taking 70% of the load time and 30% is spent on the javascript portion, and I can post the log if requested.

Note: This project was based on the work of Mohit Shrivastav. About 1/2 way through the project, I decided that I wanted to use datatables.net instead of Angular to do the sorting since I'm still learning Angular.

Here's the Controller:

public with sharing class recentMqlExplorerController {
  public String LeadContactList {
    get; set;
  }

  public class ContactLeadWrap {
    public String id;
    public String name;
    public String dateofmql;
    public String company;
    public Decimal leadscore;
    public String country;
    public String state;
    public Integer employees;
    public String ownername;
    public String leadstatus;
    public String leadorcontact;
    public String team;
    public String url;

    ContactLeadWrap() {
      id = '';
      name = '';
      company = '';
      leadscore = 0;
      country = '';
      state = '';
      employees = 0;
      ownername = '';
      leadstatus = '';
      url = '';
      team = '';
    }
  }

  //Method to bring the list of Contacts and Leads and Serialize Wrapper Object as JSON
  public  static String getlstContactLead() {
    List <ContactLeadWrap> lstwrap = new List <ContactLeadWrap> ();
    List <Contact> lstcontact = new List<Contact>();
    List <Lead> lstlead = new List<Lead>();
    Datetime now = System.now();
    Datetime last = now.addDays(-60);
    Id ownerId = UserInfo.getUserId();


    lstcontact = [SELECT Id, FirstName, Name, Date_Became_MQL__c, Account.Name, mkto2__Lead_Score__c, Account.BillingCountry, Account.BillingState, Account.NumberOfEmployees, Owner.Name, z_Lead_Status_at_Convert_TEXT_if_applcbl__c, Account.AOU__r.sub_sub_Team_Picklist__c FROM Contact WHERE (Date_Became_MQL__c > :last AND Owner.Id = :ownerId) OR FirstName = 'westest' LIMIT 20000];

    lstlead = [SELECT Id, FirstName, Name, Date_Became_MQL__c, Company, mkto2__Lead_Score__c, Country, State, NumberOfEmployees, Owner.Name, status, z_Lead_Owner_User__r.sub_sub_Team_Picklist__c FROM Lead  WHERE (Date_Became_MQL__c > :last AND IsConverted = false AND Owner.Id = :ownerId) OR (FirstName = 'westest' AND IsConverted = false) LIMIT 20000];


    String convertedDateofmql = null;
    Date dateOnly = null;
    String fullRecordURL = null;

    for (Contact c: lstcontact) {
        if (c.Date_Became_MQL__c != null) {
            convertedDateofmql = c.Date_Became_MQL__c.format('MM/dd/YYYY');
        }
        if (c.z_Lead_Status_at_Convert_TEXT_if_applcbl__c == null) {
            c.z_Lead_Status_at_Convert_TEXT_if_applcbl__c = 'no status';
        }
        fullRecordURL = URL.getSalesforceBaseUrl().toExternalForm() + '/' + c.Id;
        ContactLeadWrap cwrap = new ContactLeadWrap();
        cwrap.id = c.id;
        cwrap.name = c.name;
        cwrap.url = fullRecordURL;
        cwrap.dateofmql = convertedDateofmql;
        cwrap.company = c.Account.Name;
        cwrap.leadscore = c.mkto2__Lead_Score__c;
        cwrap.country = c.Account.BillingCountry;
        cwrap.state = c.Account.BillingState;
        cwrap.employees = c.Account.NumberOfEmployees;
        cwrap.ownername = c.Owner.Name;
        cwrap.leadstatus = c.z_Lead_Status_at_Convert_TEXT_if_applcbl__c;
        cwrap.leadorcontact = 'Contact';
        cwrap.team = c.Account.AOU__r.sub_sub_Team_Picklist__c;
        lstwrap.add(cwrap);
    }
    for (Lead l: lstlead) {
        if (l.Date_Became_MQL__c != null) {
            convertedDateofmql = l.Date_Became_MQL__c.format('MM/dd/YYYY');
        }
        fullRecordURL = URL.getSalesforceBaseUrl().toExternalForm() + '/' + l.Id;
        ContactLeadWrap lwrap = new ContactLeadWrap();
        lwrap.id = l.id;
        lwrap.name = l.name;
        lwrap.url = fullRecordURL;
        lwrap.dateofmql = convertedDateofmql;
        lwrap.company = l.Company;
        lwrap.leadscore = l.mkto2__Lead_Score__c;
        lwrap.country = l.Country;
        lwrap.state = l.State;
        lwrap.employees = l.NumberOfEmployees;
        lwrap.ownername = l.Owner.Name;
        lwrap.leadstatus = l.status;
        lwrap.leadorcontact = 'Lead';
        lwrap.team = l.z_Lead_Owner_User__r.sub_sub_Team_Picklist__c;
        lstwrap.add(lwrap);
    }
    return JSON.serialize(lstwrap);
  }

  public class getInformation {

  }
}

Here's the Page:

<apex:page standardStylesheets="false" sidebar="false" showHeader="false" docType="html-5.0" controller="recentMqlExplorerController">
    <html xmlns:ng="http://angularjs.org" ng-app="hello" lang="en">
      <head>
        <meta charset="utf-8"></meta>
        <meta http-equiv="X-UA-Compatible" content="IE=edge"></meta>
        <title>MQL Explorer</title>
        <meta name="description" content=""></meta>
        <meta name="viewport" content="width=device-width"></meta>
          <link rel="stylesheet" href="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/sass-bootstrap/dist/css/bootstrap.css')}"/>
          <link rel="stylesheet" href="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/styles/main.css')}"/>
          <link type='text/css' href='//fonts.googleapis.com/css?family=Open+Sans:300italic,400,300' rel='stylesheet'/>
      </head>
      <body>
          <!-- =========== Binding Controller to Body of Page ============= -->
            <div class="jumbotron ribbon">
            <h2>Hello {!$User.FirstName}:</h2> <h4> Welcome to the Recent MQL Dashboard</h4>
        </div>
        <div class="jumbotron banner row">
            <span class="chevron"><i class="fa fa-chevron-down fa-1x"></i></span>
            <div class="jumbotron jbot"></div>
                    <div class="dashboard">
                    </div>
                    <div class="dashboard-shadow"></div>
        </div>
          <div ng-controller="ctrlRead" class="container table-container">
                <div class="table-container-header">
                        <h4>Qualified Leads and Contacts</h4>
                </div>
              <table id="dashboard" class="table-borders display responsive" width="100%">
                <thead>
                    <tr role="row" class="tableFilters">
                        <th>Search by Name</th>
                        <th>From when?</th>
                        <th>Search by Company</th>
                        <th></th>
                        <th></th>
                        <th></th>
                        <th></th>
                        <th id="owner">Search by Owner</th>
                        <th>What Status?</th>
                        <!-- <th>What Type?</th> -->
                        <th>What Team?</th>
                    </tr>
                    <tr role="row" class="tableHeader">
                        <th scope="col" class="name">Name</th>
                        <th scope="col" class="dateofmql" width="250px">Date of MQL</th>
                        <th scope="col" class="company">Company</th>
                        <th scope="col" class="leadscore">Score</th>
                        <th scope="col" class="country">Country</th>
                        <th scope="col" class="state">State</th>
                        <th scope="col" class="employees">EE#</th>
                        <th scope="col" class="ownername">Owner Name</th>
                        <th scope="col" class="leadstatus">Lead Status</th>
                        <!-- <th scope="col" class="leadorcontact">Type</th> -->
                        <th scope="col" class="team">Sales Team</th>
                    </tr>
                </thead>
              <tbody class="table">
                      <tr ng-repeat="item in pagedItems[currentPage] | orderBy:sortingOrder:reverse">
                          <th><apex:outputlink style="text-decoration: underline; color: #01B2E4;" target="_blank" value="{{item.url}}">{{item.name}}</apex:outputlink></th>
                          <td>{{item.dateofmql}}</td>
                          <td>{{item.company}}</td>
                          <td>{{item.leadscore}}</td>
                          <td>{{item.country}}</td>
                          <td>{{item.state}}</td>
                          <td>{{item.employees}}</td>
                          <td>{{item.ownername}}</td>
                          <td>{{item.leadstatus}}</td>
                          <td>{{item.team}}</td>
                      </tr>
                  </tbody>
              </table>
            </div>

        <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/angular/angular.min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/jquery/jquery.min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/jquery/jquery-ui.min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/jquery/jquery.dataTables.min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/jquery/dataTables.tableTools.min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/jquery/dataTables.responsive.min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/jquery/jquery.dataTables.columnFilter-min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/bower_components/angular-force/ngForce.min.js')}"/>
          <script src="{!URLFOR($Resource.smbMqlHunterAppMarkIiiDEV, '/scripts/app-min.js')}"/>
          <script type="text/javascript">
                'use strict';
                <!-- Name your application -->
                var myapp = angular.module('hello', []);
                var sortingOrder = 'name';
                <!-- Define Controller  -->
                var contrl=myapp.controller('ctrlRead', function ($scope, $filter) {
                <!--- Initialize Scope Variables --->
                $scope.sortingOrder = sortingOrder;
                $scope.reverse = false;
                $scope.filteredItems = [];
                $scope.groupedItems = [];
                $scope.itemsPerPage = 60000;
                $scope.pagedItems = [];
                $scope.currentPage = 0;
                $scope.items ={!lstContactLead};
                var searchMatch = function (haystack, needle) {
                    if (!needle) {
                        return true;
                    }
                    return haystack.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
                };
                //Initialize the Search Filters
                $scope.search = function () {
                    $scope.filteredItems = $filter('filter')($scope.items, function (item) {
                        for (var attr in item) {
                            if (searchMatch(item[attr], $scope.query))
                                return true;
                        }
                        return false;
                    });
                    // Define Sorting Order
                    if ($scope.sortingOrder !== '') {
                        $scope.filteredItems = $filter('orderBy')($scope.filteredItems, $scope.sortingOrder, $scope.reverse);
                    }
                    $scope.currentPage = 0;

                    // Group by pages
                    $scope.groupToPages();
                };
                // Calculate Total Number of Pages based on Records Queried
                $scope.groupToPages = function () {
                    $scope.pagedItems = [];
                    for (var i = 0; i < $scope.filteredItems.length; i++) {
                        if (i % $scope.itemsPerPage === 0) {
                            $scope.pagedItems[Math.floor(i / $scope.itemsPerPage)] = [$scope.filteredItems[i]];
                        } else {
                            $scope.pagedItems[Math.floor(i / $scope.itemsPerPage)].push($scope.filteredItems[i]);
                        }
                    }
                };

                $scope.range = function (start, end) {
                    var ret = [];
                    if (!end) {
                        end = start;
                        start = 0;
                    }
                    for (var i = start; i < end; i++) {
                        ret.push(i);
                    }
                    return ret;
                };

                  $scope.prevPage = function () {
                      if ($scope.currentPage > 0) {
                          $scope.currentPage--;
                      }
                  };

                  $scope.nextPage = function () {
                      if ($scope.currentPage < $scope.pagedItems.length - 1) {
                          $scope.currentPage++;
                      }
                  };
                  $scope.setPage = function () {
                      $scope.currentPage = this.n;
                  };
                  // functions have been describe process the data for display
                  $scope.search();

                  // change sorting order
                  $scope.sort_by = function (newSortingOrder) {
                      if ($scope.sortingOrder == newSortingOrder)
                          $scope.reverse = !$scope.reverse;
                      $scope.sortingOrder = newSortingOrder;

                      // icon setup
                      $('th i').each(function () {
                          // icon reset
                          $(this).removeClass().addClass('icon-sort');
                      });
                      if ($scope.reverse)
                          $('th.' + new_sorting_order + ' i').removeClass().addClass('icon-chevron-up');
                      else
                          $('th.' + new_sorting_order + ' i').removeClass().addClass('icon-chevron-down');
                    };
                });
                contrl.$inject = ['$scope', '$filter'];
          </script>
        </body>
    </html>
</apex:page>

Upvotes: 0

Views: 1110

Answers (2)

user3440239
user3440239

Reputation: 78

Just by taking a cursory look into your code following are my observations.

1) You are using too many scripts stored in you scripts such as angular, jquery, Bootstrap etc. Using these many scripts are making a time lag. Not sure of your requirement, but look for the way to constrain use of external scripts.

2) Not using Salesforce tags - Almost complete page is JAVA based which in inserted into apex tags. Eg Use apex tables instead of using table tr and td tags.

Using standard out of box features will try to reduce the load time of the page, and try to leverage native Salesforce scripts.

3) I would even suggest using a custom script instead of bootstrap if you are not using a lot of tags. When in Salesforce try to keep the look and feel that of Salesforce, which will give best results.

Disclaimer - These are general suggestions for optimizing page loads, although specific requirements may force to use native scripts.

Ashish

Upvotes: 1

naveen
naveen

Reputation: 159

Please have a look at below guidelines provided by salesforce it may be helpful to improve page performance.

Large page sizes directly affects load times. To improve Visualforce page load times:

Cache any data that is frequently accessed, such as icon graphics.

Avoid SOQL queries in your Apex controller getter methods.

Reduce the number of records displayed on a page by:

Limiting the data coming back from SOQL calls in your Apex controllers. For example, using AND statements in your WHERE clause, or removing null results

Taking advantage of pagination with a list controller to present fewer records per page

“Lazy load” Apex objects to reduce request times.

Consider moving any JavaScript outside of the tag and placing it into a tag right before your closing tag. The tag places JavaScript right before the closing element; thus, Visualforce attempts to load the JavaScript before any other content on the page. However, you should only move JavaScript to the bottom of the page if you’re certain it doesn’t have any adverse effects to your page. For example, JavaScript code snippets requiring document.write or event handlers should remain in the element.

In all cases, Visualforce pages must be under 15 MB.

Regards,

Naveen

http://www.autorabit.com

Upvotes: 2

Related Questions