Skip to main content

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 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-PatternProblemSolution
Loading records in loopsExcessive governance useUse search instead
Getting line count in loop conditionRecalculated each iterationStore in variable
Not using filtersProcessing unnecessary dataFilter at source
Creating searches repeatedlyWasted governanceUse saved searches
Full load for simple reads10 units vs 5 unitsUse lookupFields
Full save for simple updates30 units vs 10 unitsUse submitFields

Next Steps