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
Markdown
Performance optimization is critical for delivering a responsive user experience
and staying within governor limits. This guide covers comprehensive strategies
for optimizing Salesforce applications.
**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
```
```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
```
```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'];
```
```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
// 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
}
}
```
```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, '');
}
}
```
```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
}
```
```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];
```
```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);
}
}
```
```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
);
}
}
```
```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
}
}
```
```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;
}
}
```
```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
}
}
```
```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>
```
```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);
},
}
);
```
```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>
```
```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;
}
}
```
```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;
}
}
```
```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;
}
}
```
```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');
}
}
```
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