Post Featured Image

Learn How External SuiteScripts Can Affect SuiteCommerce Performance

This blog post is generally appropriate for all versions of SuiteCommerce and SuiteCommerce Advanced.

This week, we have another guest post by Florencia Meilán, principal product manager, who's sharing more of her wisdom. She's going to explain how your backend scripts (search as scriptable cart, user scripts, and client scripts) can impact your site's performance, and what you can do to troubleshoot, mitigate, and avoid performance problems. She's not going to talk about SuiteScript that runs from within the SuiteCommerce application (eg in your web store modules).

Performance is a hot topic on the developer blog because we want your sites to be fast, and we want to avoid the worst-case scenarios that can arise, such as situations where shoppers are blocked for particularly long times from using the site. For example, a common problem might be slowness after hitting a submit button where the shopper has to wait 5, 10, 20 seconds, or even longer, before the site appears to respond.

We have a dedicated performance section that encompasses a variety of different areas of site performance, and we've covered practical tips for troubleshooting SuiteScript performance before (for example, when we looked at console timers). This time, Flo is going to cover SuiteScripting in detail, in particular scripts that apply to backend UI operations as well as frontend ones.

Introduction

Ecommerce applications built on the NetSuite platform can run fast and perform reliably, as long as you use the platform within its capabilities and follow best practices. The following information is intended to help you identify permissible designs that ensure the scalability and robustness of your customizations.

An important thing to remember is that it is OK to sometimes make a decision that will be at a cost of performance, so long as you are aware of that cost. Once you know that price you can prioritize, and weigh the cost against the benefits of each of your customizations.

In general, the keys to great performance are:

  1. Establishing performance as a priority, and doing it at the start of a project
  2. Ensuring everyone knows that performance is their responsibility too
  3. Factoring in performance costs when prioritizing features

Which SuiteScripts are the Most Likely Cause Poor Web Store Performance?

If you're experiencing slowness, tracking down where it's coming from can sometimes be tricky. In my experience, the following script types are the best places to start:

  1. User event SuiteScripts deployed against sales order or customer records, or forms used by the web store
  2. Workflows running against sales order or customer records
  3. Customized sales order records that have custom fields
  4. Client SuiteScripts deployed against sales order records or forms used by the web store (specifically cart and checkout performance when scriptable cart is enabled)
  5. SuiteBundles that contain any of the above scripts

When analyzing the situation, you should also think about what step in the process the slowness is occurring because different types of customizations can affect different areas. The table below summarizes the possibilities:

StepScriptable CartCustomer User Event Scripts / WorkflowsSales Order User Event Scripts / Workflows
Home page
Item list
Add to cartYes
View cart
Proceed to checkoutYes
Register/login pageYesYes
Add address > select shipping methodYes
Select shipping method
Add payment method > review & submitYes
Submit orderYesYes

What you can see above is that if you enable scriptable cart, it will trigger on most pages throughout a shopper's journey. Any scripts in SS/SSP files are often triggered on touchpoints, and Suitelet/RESTlet scripts can also be triggered on the web store context.

Customer records and user event scripts can be triggered when the shopper logs in, or registers a new account.

Finally, the moment the user submits an order, sales order user events scripts and workflows will trigger, which can be particularly problematic.

What are the Indicators of Slowness?

It is your responsibility to ensure that the SuiteApps and scripts you deploy into a NetSuite instance (whether yours or a customer's) have efficient and performant code.

The short answer the above question is that the script is taking a long time to run but not failing, manifesting in web page latency.

If the script is taking a long time to run, then it's probably doing one or more of the following:

  1. Performing a large number of record operations without going over the usage limit
  2. Triggering a large number of other user event scripts / workflows to execute
  3. Performing a large number of database searches that take a long time to run

Each server-side script type or application has a time limit for execution. This limit is not fixed and depends on the script type or application. If a single execution of a script or application takes longer than the time limit for that script type or application, an SSS_TIME_LIMIT_EXCEEDED error is thrown. This error can also be thrown from a script that is executed by another script (for example, from a user event script that is executed by a scheduled script).

SuiteScript Best Practices for the Web Store Context

So, once you've identified a possible cause for your performance issues, what can you do about it?

Broadly speaking, there are five areas you can make changes to:

  1. Configuration — changes in the NetSuite UI that determine how the script is run
  2. Code-level — changes to the general way you've written your script
  3. Searches — changes to the way you're searching for records or field data
  4. Governance — changes to the resources your scripts are using, to avoid going over the usage limits
  5. Standardization — changes to the way you've implemented your script, so that you use more standard NetSuite functionality instead of your customization

Configuration

You can first check to see how your script is running and make decisions about whether you've configured it correctly.

Prevent the Script from Running in the Web Store Context

If the script is only required when orders are being placed in the backend (ie by staff members), then you can disable it from running on the frontend.

On the deployment page for the script, it is common for all roles in the Audience > Roles multiselect to be selected. If you uncheck the Select All checkbox and, instead, select only All Employees, this will prevent the script from running on the frontend.

For user event scripts, there is another option you can use: the Execute in Commerce Context checkbox. When checked, the script will execute on SuiteCommerce, SuiteCommerce Advanced and SuiteCommerce InStore sites; when unchecked, it won't. Again, if your script isn't needed on the frontend, consider preventing the script from running in that context.

Prevent a Workflow from Running in the Web Store Context

For similar reasons as above, you can also prevent a workflow from triggering in events that happen on the frontend.

In the Contexts multiselect, de-select the Web Store option.

Specify the Event Type on the Deployment Record

The Event Type dropdown appears on Suitelet, user event, and record-level client script deployment pages. You can use it to specify the script's execution context at the time of deployment. Once you specify the event time, the deployed script will only execute on that event, regardless of what's specified in he script.

It's important that you remember that whatever you select in the UI will take precedence over what's specified in the file. For example, if you've specified the Create type in the script, selecting Delete in the UI will restrict the script from running in any event other than the Delete one.

Leaving the dropdown empty will run the script in the event types specified specified in the script.

Use Asynchronous afterSubmit User Events

If you don't enable asynchronous execution, then your script will be processed synchronously — that means that the system will wait for the entire script to run before proceeding, blocking the user. Enabling asynchronous execution means that the normal processes will complete, while your script runs in the background. Consider using this if the outcome of your script doesn't impact the submission of the record/order.

Code-Level Best Practices

When developing SuiteScript, it is important that you use only supported and documented APIs, methods and objects (ie the ones documented in the help center). Keep in mind, too, that DOM references in SuiteScript are not supported.

We strongly recommend using SuiteScript 2.0 for new scripts that you develop, and that you consider converting your SuiteScript 1.0 scripts to SuiteScript 2.0. We'll continue to support SuiteScript 1.0, and if you're just tinkering with an existing script, you can continue to use them, but you should use 2.0 for any substantially revised script.

You cannot use SuiteScript 2.0 user event scripts and client scripts at the same time for scriptable cart; in most cases, it is better to use SuiteScript 1.0 for user event scripts and SuiteScript 2.0 for client event scripts, but the reverse is also supported. Also note that at the time of writing, you cannot use 2.0 within the SuiteCommerce SSPs.

Refer to our API maps to see how the two compare:

The following examples are a mix of SuiteScript 1.0 and SuiteScript 2.0.

Branch Scripts Depending on the Metadata or Context of Execution

This means that your scripts first check where it is being accessed from before running; or, to put it another way, run different code depending on whether it is run from web store or the backed UI.

  • SuiteScript 1.0nlapiGetContext()
  • SuiteScript 2.0runtime.getCurrentScript(), runtime.getCurrentSession(), runtime.getCurrentUser()

Create a branch at the onset of the function call, and then determine the succeeding logic per script context — this is particularly important for client scripts executing via scriptable cart or in the checkout.

For example, using SuiteScript 1.0 we might write:

function casePageInit (type)
{
  var currentContext = nlapiGetContext();
  if (currentContext.getExecutionContext() == 'webstore')
  {
    //add logic here
  }
  else
  {
    //either exit or perform other logic
  }
}

Use Setter and Getter Methods

It is often better to use the built-in getter/setter methods than to manually try to get/set these values.

For example: nlapiGetFieldValue/nlapiSetFieldValue, nlapiGetLineItemCount/nlapiSetLineItemCount, and nlapiGetLineItemValue/nlapiSetLineItemValue when working on fields for current record.

This removes overhead from loading entire record objects.

Use Caching

In SuiteScript 1.0, use cached variables as applicable. This eliminates calling the same function/method which will return the same response on a single instance of script execution.

In 2.0, the cache API enables you to load data into a cache and make it available to one or more scripts. This feature reduces the amount of time required to retrieve data.

Load the cache module to enable temporary, short-term storage of data, which is stored according to its specified time to live (TTL). The cache module is supported by all server-side script types, and can improve performance by eliminating the need for scripts in your account to retrieve the same piece of data more than a single time.

Be Mindful of nlapiSearchRecord's Limitation of 1000 Rows

Results are limited to the first 1000 results when using nlapiSearchRecord in SuiteScript 1.0, or the N/search module in SuiteScript 2.0.

SuiteScript 2.0 has a search pagination API that enables you to page through search results, which improves performance and gives you an intuitive way to efficiently traverse search result data.

Consolidate afterSubmit Scripts that Perform Similar Record Operation Calls

Let's say, for example, that you have four separate scripts that have similar logic of comparing old and new record data, or it needs to iterate through a sublist with separate nlapiGetOldRecord, nlapiGetNewRecord, nlapiLoadRecord calls in each afterSubmit script. Consider consolidating these down to a single script that combines the logic of all four scripts. You will minimize overhead by reducing the number of API calls of individual scripts.

Use Asynchronous Processing via Scheduled Scripts

If your script performs numerous record updates or POST operations after submission, consider moving it to a scheduled script.

In this example, you could have a sales order afterSubmit script that sends an email to a sales rep, creates a custom record for tracking, performs a call to an external site, etc — ask yourself whether this needs to happen immediately after the user has hit submit, or whether it can be performed outside of the process at another time.

Use the Field Lookup and Field Submit Methods to Update Single Records and Fields

Loading and submitting entire records is costly, so if you're just loading/updating a single field (or two) consider using the built-in methods:

  • SuiteScript 1.0nlapiLookupField to get a field; nlapiSubmitField to set a field
  • SuiteScript 2.0search.lookupFields to get a field; search.submitFields to a set field

Generally speaking, these two types of methods consume far fewer resources and yield better performance as a result.

//performing a lookup for a field / array of fields
var lookupFields = ['email', 'phone', 'entityid'];
var columns = nlapiLookupField ('customer', customer_id, lookupFields);
var email = columns.email;
var phone = columns.entityid;

//updating a field / array of fields
var fields = new Array();
var values = new Array();
fields[0] = 'phone';
values[0] = "0123456789";
fields[1] = 'url';
values[1] = "www.example.com";
fields[2] = 'billpay';
values[2] = "T";
nlapiSubmitField('customer', 123, fields, values);

Convert afterSubmit Logic to beforeSubmit

This will remove overheads from load/set/submit records on afterSubmit.

Search Best Practices

Avoid Performing a Search per Individual Line Item

Build an array of item records and use that as a filter to execute only one search for all records, instead of performing individual searches for line.

Consolidate Multiple Searches into One Search with a Broader Condition

In other words, reduce the number of calls you make. If your script performs multiple searches for the same record, manipulate the returned search result in each of the different cases, rather than performing the search repeatedly.

Avoid Loading the Record of Each Search Result

After performing a search, do not use nlapiLoadRecord or nlapiLookupField to load the record into memory to retrieve data — it is very inefficient because of the unnecessary number of I/O API calls. Instead, prior to running the search, add the desired columns to the search (and use getValue!)

For example, compare the unoptimized script (top) with the optimized script (bottom):

// Unoptimized script
var filters = new Array();
filters.push(new nlobjSearchFilter('balance', null, 'between', 0, 1000));

var results = nlapiSearchRecord('customer'), null, filters, null);
for (var i=0; results != null && results.length < i; i++)
{
  var recId = results[i].getId();
  //using search results to help load records in a for loop is very innefficient
  var customer = nlapiLoadRecord ('customer', recId);
  var entityID = customer.getFieldValue ('entityid')
}

// Optimized script
var filters = new Array();
filters.push(new nlobjSearchFilter ('balance', null, 'between', 0, 1000));

//adding the desired columns in the search gives you the desired data and avoids loading records unnecessarily
var columns = new Array();
columns.push(new nlobjSearchColumn('entityid', null, null));

var results = nlapiSearchRecord('customer', null, filters, columns);
for (var i=0; results != null && results.length < i; i++)
{
  var entityId = results[i+1].getValue('entityid')
}

SuiteScript Governance Best Practices

You must always keep in mind the governance usage limits of executing SuiteScript.

When a script exceeds the governance limits, the system throws a usage limit error, and the script is halted by the platform. The script cannot be resumed at the point of file and, as a result, any processes that depend on that script can be terminated mid-stream. When this happens, you threaten the integrity of your data and any downstream tasks and transactions.

Minimize Calls to Third-Party URLs

Third-party services can be unreliable, or slow, and any performance problems they have will be transferred to the end user.

Where possible, avoid making these calls (eg via nlapiRequestURL) and, if possible, use inline/global variables instead.

Use Scheduled Scripts for High Volume I/O Tasks

Scheduled scripts have a much higher governance usage limit (10,000 units per script), which gives you a lot more resources to do your processing. You can write your user event scripts and Suitelets so that the 'heavy lifting' (ie high volume I/O calls) is done by a scheduled script.

The caller script can call the scheduled script with nlapiScheduleScript; once it's made the call, the control returns immediately back to the caller script, thereby improving user experience — the user does not need to wait for the logic to finish executing. The invoked scheduled script is then placed in a queue and executed asynchronously.

Just note that nlapiScheduleScript is not currently available within SSP applications, but it can be called within Suitelet, RESTlet, user event, and portlet scripts.

Use a Checker Function to Yield if Necessary (SuiteScript 1.0)

If you do use scheduled scripts, make sure you add a function that checks how much governance units you have left, and then yields if it gets low.

Yielding is a feature of scheduled scripts that creates a recovery point for a scheduled script, and then schedules another time for it to run later. In other words, while it may not finish running all of the I/Os, it will do as many as possible and do the rest later which means, importantly, that the data retains its integrity.

Note that yielding is a built-in feature of SuiteScript 2.0, so this only applies to SuiteScript 1.0.

An example of this script:

if (context.getRemainingUsage() <= 50)
{
  var stateMain = nlapiYieldScript();
  if( stateMain.status == 'FAILURE')
  {
    nlaplogExecution("debug", "Failed to yield script (do-while), exiting: Reason = "+ stateMain.reason + " / Size = "+ stateMain.size);
    throw "Failed to yield script";
  }
  else if ( stateMain.status = 'RESUME')
  {
    nlaplogExecution ("debug", "Resuming script (do-while) because of " + stateMain.reason+" .Size = "+ stateMain.size);

  }
}

Process Large Amounts of Data with Map/Reduce (SuiteScript 2.0)

The map/reduce script type is designed for scripts that need to handle large amounts of data. It is best suited for situations where the data can be divided into small, independent parts. When the script is executed, a structured framework automatically creates enough jobs to process all of these parts. You as the user do not have to manage this process. Another advantage of map/reduce is that these jobs can work in parallel. You choose the level of parallelism when you deploy the script.

If a map/reduce job violates certain aspects of NetSuite governance, the map/reduce framework automatically causes the job to yield and its work to be rescheduled for later, without disruption to the script. However, be aware that some aspects of map/reduce governance cannot be handled through automatic yielding. For that reason, if you use this script type, you should familiarize yourself with the SuiteScript 2.0 Map/Reduce Governance guidelines.

Since the new Map/Reduce script automatically handles governance usage and yielding, nlapiSetRecoveryPoint and nlapiYieldScript has been deprecated in SS 2.0.

Use as Much Standard Functionality as Possible

Consider whether custom records are necessary: consider, instead, whether it would be better to add a custom field to an existing record. Create new records when you need a new business object that is not aligned with a standard object.

Keep your web store transaction forms lightweight: remove all fields that are not necessary (including standard forms).

Final Thoughts

SuiteScript forms an important part of any SuiteCommerce site — including the backend — and it's vitally important that you ensure your code is streamlined and performant.

As previously mentioned, we have a number of resources on performance available on the site, in the form of documentation, blog posts and the performance checklist.