Performance Optimization
Optimizing SuiteScript performance ensures scripts run efficiently within governance limits and provide good user experience.
Governance Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ GOVERNANCE LIMITS BY SCRIPT TYPE │
└─────────────────────────────────────────────────────────────────────────────┘
Script Type Governance Units Timeout
────────────────────── ───────────────── ─────────────
Suitelet 10,000 15 min
User Event 1,000 15 min
Client Script N/A N/A
Scheduled Script 10,000 60 min
Map/Reduce (per stage) 10,000 60 min
RESTlet 5,000 15 min
Workflow Action 1,000 15 min
COMMON OPERATION COSTS
──────────────────────────────────────
search.create() 5 units
search.load() 10 units
record.load() 10 units
record.save() 20 units
record.delete() 20 units
record.submitFields() 10 units
email.send() 20 units
http.get/post() 10 units
Governance Monitoring
/**
* Governance monitoring utility
*/
const checkGovernance = (threshold = 100) => {
const script = runtime.getCurrentScript();
const remaining = script.getRemainingUsage();
if (remaining < threshold) {
log.debug('Low Governance', `Only ${remaining} units remaining`);
return false;
}
return true;
};
/**
* Log governance at key points
*/
const logGovernance = (stage) => {
const remaining = runtime.getCurrentScript().getRemainingUsage();
log.debug('Governance', `${stage}: ${remaining} units remaining`);
};
// Usage
const execute = (context) => {
logGovernance('Start');
records.run().each((result, index) => {
if (index % 100 === 0) {
logGovernance(`Processed ${index}`);
}
if (!checkGovernance(200)) {
// Reschedule for remaining records
rescheduleScript(result.id);
return false;
}
processRecord(result);
return true;
});
logGovernance('End');
};
Record Operations Optimization
Use submitFields Instead of Load/Save
// GOOD: submitFields for simple updates (10 units)
record.submitFields({
type: record.Type.CUSTOMER,
id: customerId,
values: {
'custentity_status': 'Active',
'custentity_last_update': new Date()
}
});
// AVOID: Full load/save for simple updates (30 units)
const customer = record.load({
type: record.Type.CUSTOMER,
id: customerId
});
customer.setValue({ fieldId: 'custentity_status', value: 'Active' });
customer.save();
Lookup Fields Instead of Loading Records
// GOOD: lookupFields for reading (5 units)
const values = search.lookupFields({
type: search.Type.CUSTOMER,
id: customerId,
columns: ['companyname', 'email', 'phone']
});
// AVOID: Full load just to read values (10 units)
const customer = record.load({
type: record.Type.CUSTOMER,
id: customerId
});
const name = customer.getValue({ fieldId: 'companyname' });
Dynamic Mode for Reading Records
// GOOD: Dynamic mode when only reading (faster)
const invoice = record.load({
type: record.Type.INVOICE,
id: invoiceId,
isDynamic: true // Faster for read operations
});
// Standard mode when modifying sublists
const invoice = record.load({
type: record.Type.INVOICE,
id: invoiceId,
isDynamic: false // Required for sublist modifications
});
Search Optimization
Limit Columns
// GOOD: Only request needed columns
const orderSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [['mainline', 'is', 'T']],
columns: ['tranid', 'entity', 'total'] // Only what you need
});
// AVOID: Requesting unnecessary columns
const orderSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [['mainline', 'is', 'T']],
columns: [
'tranid', 'entity', 'total', 'subsidiary',
'department', 'class', 'location', 'memo',
'terms', 'duedate', 'shipdate', 'shipmethod',
// ... many more columns you don't use
]
});
Use Filters Effectively
// GOOD: Let NetSuite filter data
const recentOrders = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', 'lastmonth'],
'AND',
['status', 'anyof', 'SalesOrd:B', 'SalesOrd:D']
]
});
// AVOID: Fetching all and filtering in JavaScript
const allOrders = search.create({
type: search.Type.SALES_ORDER,
filters: [['mainline', 'is', 'T']]
});
allOrders.run().each((result) => {
const date = result.getValue('trandate');
const status = result.getValue('status');
// Filtering in JS wastes governance
if (isLastMonth(date) && isValidStatus(status)) {
processOrder(result);
}
return true;
});
Pagination for Large Datasets
/**
* Efficient pagination pattern
*/
const processLargeDataset = () => {
const mySearch = search.create({
type: search.Type.TRANSACTION,
filters: [/* ... */],
columns: [/* ... */]
});
// Use paged data for large datasets
const pagedData = mySearch.runPaged({ pageSize: 1000 });
log.debug('Search Results', `${pagedData.count} total results`);
pagedData.pageRanges.forEach((pageRange) => {
const page = pagedData.fetch({ index: pageRange.index });
page.data.forEach((result) => {
processResult(result);
});
});
};
Saved Searches
// GOOD: Load existing saved search (10 units, but cached)
const savedSearch = search.load({ id: 'customsearch_open_orders' });
// vs Creating search each time (5 units, but repeated)
const createdSearch = search.create({
type: search.Type.SALES_ORDER,
// ... same filters every time
});
Batch Operations
Process in Batches
/**
* Batch update pattern
*/
const batchUpdate = (recordIds, batchSize = 50) => {
const results = { success: 0, failed: 0 };
for (let i = 0; i < recordIds.length; i += batchSize) {
const batch = recordIds.slice(i, i + batchSize);
batch.forEach(id => {
try {
record.submitFields({
type: record.Type.CUSTOMER,
id: id,
values: { 'custentity_processed': true }
});
results.success++;
} catch (e) {
results.failed++;
}
});
// Check governance after each batch
if (!checkGovernance(500)) {
log.debug('Batch Interrupted', `Processed ${i + batch.length} of ${recordIds.length}`);
return { ...results, incomplete: true, lastIndex: i + batchSize };
}
}
return results;
};
Parallel Processing with Map/Reduce
┌─────────────────────────────────────────────────────────────────────────────┐
│ MAP/REDUCE PARALLEL PROCESSING │
└─────────────────────────────────────────────────────────────────────────────┘
SEQUENTIAL (Scheduled Script):
────────────────────────────────────────────
Record 1 → Record 2 → Record 3 → Record 4 → ...
────────────────────────────────────────────
Time: Long (processes one at a time)
PARALLEL (Map/Reduce):
────────────────────────────────────────────
Thread 1: Record 1 → Record 5 → Record 9
Thread 2: Record 2 → Record 6 → Record 10
Thread 3: Record 3 → Record 7 → Record 11
Thread 4: Record 4 → Record 8 → Record 12
────────────────────────────────────────────
Time: Shorter (multiple threads)
/**
* Map/Reduce for parallel processing
*/
const getInputData = () => {
return search.create({
type: search.Type.CUSTOMER,
filters: [['isinactive', 'is', 'F']],
columns: ['internalid']
});
};
const map = (context) => {
// Each record processed in parallel
const customerId = context.value.id;
try {
updateCustomer(customerId);
context.write({ key: 'success', value: customerId });
} catch (e) {
context.write({ key: 'error', value: { id: customerId, error: e.message }});
}
};
const summarize = (summary) => {
let successCount = 0;
summary.output.iterator().each((key, value) => {
if (key === 'success') successCount++;
return true;
});
log.audit('Complete', `Processed ${successCount} customers`);
};
Caching Strategies
Script Parameter Cache
/**
* Cache configuration values
*/
let cachedConfig = null;
const getConfig = () => {
if (cachedConfig) return cachedConfig;
const script = runtime.getCurrentScript();
cachedConfig = {
threshold: script.getParameter({ name: 'custscript_threshold' }),
email: script.getParameter({ name: 'custscript_notify_email' }),
mode: script.getParameter({ name: 'custscript_mode' })
};
return cachedConfig;
};
Search Results Cache
/**
* Cache search results
*/
const cache = {};
const getCachedLookup = (recordType, id, columns) => {
const cacheKey = `${recordType}_${id}`;
if (cache[cacheKey]) {
return cache[cacheKey];
}
const values = search.lookupFields({
type: recordType,
id: id,
columns: columns
});
cache[cacheKey] = values;
return values;
};
N/cache Module
/**
* Using N/cache for persistent caching
*/
define(['N/cache'], (cacheModule) => {
const CACHE_NAME = 'CONFIG_CACHE';
const getFromCache = (key) => {
const myCache = cacheModule.getCache({ name: CACHE_NAME });
return myCache.get({
key: key,
loader: () => {
// This runs if key not found
return loadFromDatabase(key);
},
ttl: 3600 // 1 hour cache
});
};
const clearCache = (key) => {
const myCache = cacheModule.getCache({ name: CACHE_NAME });
myCache.remove({ key: key });
};
return { getFromCache, clearCache };
});
Sublist Performance
Efficient Line Iteration
// GOOD: Use line count once
const lineCount = record.getLineCount({ sublistId: 'item' });
for (let i = 0; i < lineCount; i++) {
const item = record.getSublistValue({
sublistId: 'item',
fieldId: 'item',
line: i
});
// Process...
}
// AVOID: Getting line count repeatedly in loop condition
for (let i = 0; i < record.getLineCount({ sublistId: 'item' }); i++) {
// Line count calculated every iteration!
}
Bulk Sublist Operations
/**
* Collect all data first, then process
*/
const processSublist = (rec) => {
const lines = [];
const lineCount = rec.getLineCount({ sublistId: 'item' });
// Collect all data first
for (let i = 0; i < lineCount; i++) {
lines.push({
item: rec.getSublistValue({ sublistId: 'item', fieldId: 'item', line: i }),
quantity: rec.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i }),
rate: rec.getSublistValue({ sublistId: 'item', fieldId: 'rate', line: i })
});
}
// Now process collected data
return lines.filter(line => line.quantity > 0)
.map(line => calculateDiscount(line));
};
Async Operations
Promise-Based Operations (2.1)
/**
* Parallel record loads with promises
*/
const loadMultipleRecords = async (recordIds) => {
const loadPromises = recordIds.map(id =>
record.load.promise({
type: record.Type.CUSTOMER,
id: id
})
);
// Load all in parallel
const records = await Promise.all(loadPromises);
return records;
};
Async Search
/**
* Async search execution
*/
const asyncSearch = async () => {
const mySearch = search.create({
type: search.Type.TRANSACTION,
filters: [/* ... */],
columns: [/* ... */]
});
const resultSet = await mySearch.run.promise();
const results = await resultSet.getRange.promise({ start: 0, end: 100 });
return results;
};
Performance Monitoring
/**
* Performance timing utility
*/
const measurePerformance = (operation, label) => {
const startTime = Date.now();
const startUnits = runtime.getCurrentScript().getRemainingUsage();
const result = operation();
const endTime = Date.now();
const endUnits = runtime.getCurrentScript().getRemainingUsage();
log.debug('Performance', {
label,
duration: `${endTime - startTime}ms`,
governanceUsed: startUnits - endUnits
});
return result;
};
// Usage
const result = measurePerformance(() => {
return processAllRecords();
}, 'Process All Records');
Performance Optimization Checklist
┌─────────────────────────────────────────────────────────────────────────────┐
│ PERFORMANCE OPTIMIZATION CHECKLIST │
└─────────────────────────────────────────────────────────────────────────────┘
RECORD OPERATIONS
☐ Use submitFields for simple updates
☐ Use lookupFields for reading values
☐ Avoid loading records in loops
☐ Use dynamic mode for read-only operations
SEARCHES
☐ Request only needed columns
☐ Use filters instead of JS filtering
☐ Paginate large result sets
☐ Consider saved searches for reuse
LOOPS & BATCHING
☐ Check governance inside loops
☐ Process in batches
☐ Use Map/Reduce for large datasets
☐ Cache repeated lookups
SUBLISTS
☐ Get line count once
☐ Collect data before processing
☐ Avoid modifying lines while iterating
MONITORING
☐ Log governance at key points
☐ Measure operation timing
☐ Profile slow operations
Common Performance Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Loading records in loops | Excessive governance use | Use search instead |
| Getting line count in loop condition | Recalculated each iteration | Store in variable |
| Not using filters | Processing unnecessary data | Filter at source |
| Creating searches repeatedly | Wasted governance | Use saved searches |
| Full load for simple reads | 10 units vs 5 units | Use lookupFields |
| Full save for simple updates | 30 units vs 10 units | Use submitFields |
Next Steps
- Code Standards - Coding conventions
- Error Handling - Robust error management
- Security - Security best practices