UNPKG

sf-agent-framework

Version:

AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction

718 lines (570 loc) 20.1 kB
# Salesforce Performance Optimization Guide ## Overview Performance optimization is critical for delivering a responsive user experience and staying within governor limits. This guide covers comprehensive strategies for optimizing Salesforce applications. ## Query Optimization ### SOQL Performance #### Selective Queries **Definition**: A query is selective if it uses indexed fields and returns <15% of records **Indexed Fields**: - Id (primary key) - Name (for most objects) - RecordType - Foreign key relationships - External ID fields - Unique fields - Custom fields marked as External ID **Query Selectivity Examples**: ```apex // SELECTIVE: Uses indexed Id field Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId]; // SELECTIVE: Uses indexed foreign key List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :accountId]; // NON-SELECTIVE: No indexed fields List<Account> accounts = [SELECT Id FROM Account WHERE Description LIKE '%important%']; // MAKE SELECTIVE: Add indexed field List<Account> accounts = [SELECT Id FROM Account WHERE Type = 'Customer' // Indexed AND Description LIKE '%important%']; // Then filter ``` #### Query Plan Analysis ```apex // Use Query Plan tool in Developer Console // Look for: // - TableScan (bad) vs Index (good) // - Cost > 1.0 indicates non-selective // - Leading operation type // Example optimization // BEFORE: Cost 2.4, TableScan SELECT Id FROM Opportunity WHERE Amount > 50000 // AFTER: Cost 0.8, Index SELECT Id FROM Opportunity WHERE CloseDate = THIS_YEAR AND Amount > 50000 ``` #### Relationship Query Optimization ```apex // INEFFICIENT: Multiple queries List<Account> accounts = [SELECT Id FROM Account WHERE Type = 'Customer']; for(Account acc : accounts) { List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id]; } // EFFICIENT: Single query with subquery List<Account> accounts = [SELECT Id, (SELECT Id FROM Contacts) FROM Account WHERE Type = 'Customer']; // EFFICIENT: Single query with parent lookup List<Contact> contacts = [SELECT Id, Account.Name, Account.Type FROM Contact WHERE Account.Type = 'Customer']; ``` ### SOSL Optimization #### Efficient Text Searches ```apex // Use SOSL for text searches across multiple objects List<List<SObject>> searchResults = [FIND :searchTerm IN ALL FIELDS RETURNING Account(Id, Name WHERE Type = 'Customer'), Contact(Id, Name WHERE Email != null), Opportunity(Id, Name WHERE StageName = 'Closed Won') LIMIT 100]; // Filter in SOSL rather than post-processing // WITH clauses for additional filtering List<List<SObject>> results = [FIND :searchTerm IN ALL FIELDS RETURNING Account(Id, Name) WITH DATA CATEGORY Location__c AT USA__c]; ``` ## Apex Performance ### CPU Time Optimization #### Efficient Collections ```apex // INEFFICIENT: List contains() is O(n) List<String> accountNames = new List<String>(); for(Account acc : [SELECT Name FROM Account]) { accountNames.add(acc.Name); } for(Contact con : contacts) { if(accountNames.contains(con.Account.Name)) { // O(n) operation // Process } } // EFFICIENT: Set contains() is O(1) Set<String> accountNameSet = new Set<String>(); for(Account acc : [SELECT Name FROM Account]) { accountNameSet.add(acc.Name); } for(Contact con : contacts) { if(accountNameSet.contains(con.Account.Name)) { // O(1) operation // Process } } // EFFICIENT: Map lookups Map<Id, Account> accountMap = new Map<Id, Account>([ SELECT Id, Name, Type FROM Account ]); for(Contact con : contacts) { Account acc = accountMap.get(con.AccountId); // O(1) lookup if(acc != null && acc.Type == 'Customer') { // Process } } ``` #### String Operations ```apex // INEFFICIENT: String concatenation in loops String result = ''; for(Integer i = 0; i < 1000; i++) { result += 'Item ' + i + ', '; // Creates new string each time } // EFFICIENT: Use List and join List<String> items = new List<String>(); for(Integer i = 0; i < 1000; i++) { items.add('Item ' + i); } String result = String.join(items, ', '); // EFFICIENT: StringBuilder pattern for complex concatenation public class StringBuilder { private List<String> buffer = new List<String>(); public StringBuilder append(String str) { buffer.add(str); return this; } public String toString() { return String.join(buffer, ''); } } ``` ### Memory (Heap) Optimization #### Heap Management ```apex // Monitor heap usage System.debug('Heap used: ' + Limits.getHeapSize() + ' of ' + Limits.getLimitHeapSize()); // Clear large collections when done List<Account> accounts = [SELECT Id, Name, (SELECT Id FROM Contacts) FROM Account]; processAccounts(accounts); accounts.clear(); // Free memory // Use transient for Visualforce public class MyController { public transient List<SelectOption> options {get;set;} // Not stored in view state public transient Blob fileContent {get;set;} // Large data not in view state } ``` #### Efficient Data Structures ```apex // Use appropriate collection types // List: Ordered, allows duplicates, index access // Set: Unordered, no duplicates, fast contains() // Map: Key-value pairs, fast lookups // Memory-efficient field selection // INEFFICIENT: Selecting all fields List<Account> accounts = [SELECT Id, Name, Type, Industry, Phone, Website, Description, AnnualRevenue, NumberOfEmployees FROM Account]; // EFFICIENT: Select only needed fields List<Account> accounts = [SELECT Id, Name FROM Account]; ``` ## Database Performance ### DML Optimization #### Bulk DML Operations ```apex // INEFFICIENT: Individual DML for(Contact con : contacts) { con.LastName = 'Updated'; update con; // DML in loop! } // EFFICIENT: Bulk DML List<Contact> contactsToUpdate = new List<Contact>(); for(Contact con : contacts) { con.LastName = 'Updated'; contactsToUpdate.add(con); } if(!contactsToUpdate.isEmpty()) { update contactsToUpdate; } // EFFICIENT: Partial success handling Database.SaveResult[] results = Database.update(contactsToUpdate, false); for(Integer i = 0; i < results.size(); i++) { if(!results[i].isSuccess()) { // Handle individual failures System.debug('Failed to update: ' + contactsToUpdate[i].Id); } } ``` #### Upsert Optimization ```apex // Use external IDs for upsert public class AccountImporter { public void importAccounts(List<AccountData> importData) { List<Account> accounts = new List<Account>(); for(AccountData data : importData) { accounts.add(new Account( External_ID__c = data.externalId, Name = data.name, Type = data.type )); } // Upsert using external ID Database.UpsertResult[] results = Database.upsert( accounts, Account.External_ID__c, false ); } } ``` ### Sharing Calculation #### Defer Sharing Calculation ```apex // For large data operations public class BulkDataLoader { public void loadAccounts(List<Account> accounts) { // Defer sharing calculation Database.DMLOptions dmlOptions = new Database.DMLOptions(); dmlOptions.DeferSharingCalc = true; Database.insert(accounts, dmlOptions); // Sharing will be calculated asynchronously } } ``` ## Trigger Optimization ### Efficient Trigger Patterns ```apex public class AccountTriggerHandler { // Static variables to prevent recursion private static Boolean isExecuting = false; private static Set<Id> processedIds = new Set<Id>(); public void handleAfterUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) { if(isExecuting) return; isExecuting = true; try { // Collect only records that need processing List<Account> accountsToProcess = new List<Account>(); Set<Id> accountIds = new Set<Id>(); for(Account acc : newAccounts) { if(!processedIds.contains(acc.Id) && hasRelevantChanges(acc, oldMap.get(acc.Id))) { accountsToProcess.add(acc); accountIds.add(acc.Id); processedIds.add(acc.Id); } } if(!accountsToProcess.isEmpty()) { // Bulk query related data Map<Id, List<Contact>> accountContactMap = getAccountContacts(accountIds); Map<Id, List<Opportunity>> accountOppMap = getAccountOpportunities(accountIds); // Process in bulk updateRelatedRecords(accountsToProcess, accountContactMap, accountOppMap); } } finally { isExecuting = false; } } private Boolean hasRelevantChanges(Account newAcc, Account oldAcc) { return newAcc.Type != oldAcc.Type || newAcc.Industry != oldAcc.Industry; } } ``` ## Visualforce Performance ### View State Optimization ```apex public class OptimizedController { // Transient variables not stored in view state public transient List<SelectOption> countries {get;set;} public transient Map<String, List<SelectOption>> statesByCountry {get;set;} // Only essential data in view state public String selectedCountry {get;set;} public String selectedState {get;set;} // Lazy loading pattern public List<Account> getAccounts() { if(accounts == null) { accounts = [SELECT Id, Name FROM Account WHERE Type = :selectedType LIMIT 100]; } return accounts; } private transient List<Account> accounts; // Read-only page optimization @ReadOnly public PageReference loadLargeDataSet() { // Can query up to 1 million rows // No DML operations allowed } } ``` ### Page Load Optimization ```html <apex:page controller="OptimizedController" cache="true" expires="600"> <!-- Cache page for 10 minutes --> <!-- Lazy loading with reRender --> <apex:form> <apex:commandButton value="Load Data" action="{!loadData}" reRender="dataPanel" status="loadStatus" /> </apex:form> <apex:outputPanel id="dataPanel"> <apex:pageBlock rendered="{!dataLoaded}"> <!-- Only render when data is loaded --> </apex:pageBlock> </apex:outputPanel> <!-- Use pagination for large datasets --> <apex:pageBlockTable value="{!paginatedAccounts}" var="acc"> <!-- Table content --> </apex:pageBlockTable> <!-- Efficient JavaScript remoting --> <script> Visualforce.remoting.Manager.invokeAction( '{!$RemoteAction.OptimizedController.getAccountData}', accountId, function (result, event) { if (event.status) { // Process result } }, { buffer: false, escape: true, timeout: 30000 } ); </script> </apex:page> ``` ## Lightning Performance ### Component Optimization ```javascript // Efficient data loading ({ doInit: function (component, event, helper) { // Set loading state component.set('v.isLoading', true); // Load only essential data on init helper.loadEssentialData(component).then( $A.getCallback(function () { component.set('v.isLoading', false); }) ); }, handleScroll: function (component, event, helper) { // Infinite scrolling for large datasets var scrollTop = event.target.scrollTop; var scrollHeight = event.target.scrollHeight; var clientHeight = event.target.clientHeight; if (scrollTop + clientHeight >= scrollHeight - 50) { helper.loadMoreData(component); } }, })( // Helper optimization { loadEssentialData: function (component) { return new Promise( $A.getCallback(function (resolve, reject) { var action = component.get('c.getInitialData'); action.setParams({ limitSize: 50, // Load limited data initially }); // Enable storable actions for caching action.setStorable(); action.setCallback(this, function (response) { if (response.getState() === 'SUCCESS') { component.set('v.data', response.getReturnValue()); resolve(); } else { reject(response.getError()); } }); $A.enqueueAction(action); }) ); }, // Debounce search input searchDebounced: function (component, searchTerm) { // Clear existing timeout var searchTimeout = component.get('v.searchTimeout'); if (searchTimeout) { clearTimeout(searchTimeout); } // Set new timeout searchTimeout = setTimeout( $A.getCallback( function () { this.performSearch(component, searchTerm); }.bind(this) ), 300 ); component.set('v.searchTimeout', searchTimeout); }, } ); ``` ### Lightning Data Service ```javascript // Use LDS for automatic caching and sharing <aura:component> <force:recordData aura:id="recordLoader" recordId="{!v.recordId}" targetFields="{!v.record}" layoutType="FULL" mode="VIEW"/> <!-- Data automatically cached and shared across components --> </aura:component> ``` ## Batch Processing Optimization ### Optimized Batch Implementation ```apex public class OptimizedBatch implements Database.Batchable<sObject>, Database.Stateful, Database.AllowsCallouts { // Stateful variables for cross-batch data private Map<String, Integer> summaryData = new Map<String, Integer>(); public Database.QueryLocator start(Database.BatchableContext BC) { // Use QueryLocator for large datasets (up to 50M records) // Add selective filters return Database.getQueryLocator([ SELECT Id, Name, Type, LastModifiedDate FROM Account WHERE LastModifiedDate = LAST_N_DAYS:30 AND Type != null ]); } public void execute(Database.BatchableContext BC, List<Account> scope) { // Efficient processing Map<Id, Account> accountMap = new Map<Id, Account>(scope); Set<Id> accountIds = accountMap.keySet(); // Bulk query related data Map<Id, Integer> contactCounts = getContactCounts(accountIds); Map<Id, Decimal> opportunityTotals = getOpportunityTotals(accountIds); // Process efficiently List<Account> accountsToUpdate = new List<Account>(); for(Account acc : scope) { Integer contactCount = contactCounts.get(acc.Id); Decimal oppTotal = opportunityTotals.get(acc.Id); if(updateNeeded(acc, contactCount, oppTotal)) { acc.Contact_Count__c = contactCount; acc.Total_Opportunity_Value__c = oppTotal; accountsToUpdate.add(acc); } // Update summary String key = acc.Type; summaryData.put(key, summaryData.get(key) + 1); } // Bulk update if(!accountsToUpdate.isEmpty()) { Database.update(accountsToUpdate, false); } } public void finish(Database.BatchableContext BC) { // Send summary sendSummaryEmail(summaryData); } // Efficient aggregation queries private Map<Id, Integer> getContactCounts(Set<Id> accountIds) { Map<Id, Integer> counts = new Map<Id, Integer>(); for(AggregateResult ar : [ SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :accountIds GROUP BY AccountId ]) { counts.put((Id)ar.get('AccountId'), (Integer)ar.get('cnt')); } return counts; } } ``` ## Caching Strategies ### Platform Cache ```apex public class CacheManager { private static final String PARTITION = 'local.CustomPartition'; public static Object get(String key) { return Cache.Org.get(PARTITION + '.' + key); } public static void put(String key, Object value, Integer ttl) { Cache.Org.put(PARTITION + '.' + key, value, ttl); } // Cached query pattern public static List<User> getActiveUsers() { String cacheKey = 'activeUsers'; List<User> users = (List<User>)get(cacheKey); if(users == null) { users = [SELECT Id, Name, Email FROM User WHERE IsActive = true]; put(cacheKey, users, 3600); // Cache for 1 hour } return users; } } ``` ### Custom Caching ```apex public class CustomCache { private static Map<String, CacheEntry> cache = new Map<String, CacheEntry>(); private class CacheEntry { Object value; DateTime expiry; Boolean isExpired() { return DateTime.now() > expiry; } } public static void put(String key, Object value, Integer seconds) { CacheEntry entry = new CacheEntry(); entry.value = value; entry.expiry = DateTime.now().addSeconds(seconds); cache.put(key, entry); } public static Object get(String key) { CacheEntry entry = cache.get(key); if(entry != null && !entry.isExpired()) { return entry.value; } cache.remove(key); return null; } } ``` ## Performance Monitoring ### Debug Log Analysis ```apex public class PerformanceMonitor { private Long startTime; private Map<String, Long> checkpoints; public PerformanceMonitor() { this.startTime = System.currentTimeMillis(); this.checkpoints = new Map<String, Long>(); } public void checkpoint(String name) { Long elapsed = System.currentTimeMillis() - startTime; checkpoints.put(name, elapsed); System.debug('Performance checkpoint [' + name + ']: ' + elapsed + 'ms'); // Check governor limits System.debug('SOQL: ' + Limits.getQueries() + '/' + Limits.getLimitQueries()); System.debug('DML: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements()); System.debug('CPU: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime()); System.debug('Heap: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize()); } public void logSummary() { System.debug('\n=== Performance Summary ==='); Long lastTime = 0; for(String checkpoint : checkpoints.keySet()) { Long currentTime = checkpoints.get(checkpoint); Long delta = currentTime - lastTime; System.debug(checkpoint + ': ' + delta + 'ms (total: ' + currentTime + 'ms)'); lastTime = currentTime; } System.debug('Total execution time: ' + (System.currentTimeMillis() - startTime) + 'ms'); } } ``` ## Best Practices Summary 1. **Query Optimization**: Use selective queries, indexed fields 2. **Bulkification**: Process records in bulk, avoid loops 3. **Efficient Collections**: Use appropriate data structures 4. **Lazy Loading**: Load data only when needed 5. **Caching**: Cache expensive operations 6. **Asynchronous Processing**: Use batch, queueable for large operations 7. **Field Selection**: Query only necessary fields 8. **Relationship Queries**: Use subqueries instead of multiple queries 9. **Governor Limits**: Monitor and stay within limits 10. **Performance Testing**: Test with realistic data volumes