To improve your scripts' performance, and that of the platform, please review and follow the best practices outlined below.
Working with selectors
Use filters
Use a selector's filters instead of filtering the entities yourself. Selectors let you filter by IDs and conditions. For example, you can filter by an entity's performance (return campaign's with an average CPC greater than 10), its status (campaign's that are paused), the name of the entity's parent object, and more.
Benefits of using filters:
Limits the number of entities the selector returns to only those entities that you need.
Lets the script execute faster (fewer entities to return and process)
Reduces the chance that you'll bump up against entity read limits (see Script execution limits).
Right way
var adGroups = AdsApp.adGroups()
.withCondition('Status = PAUSED')
.get();
while (adGroups.hasNext()) {
var adGroup = adGroups.next();
// Do something with paused ad group.
}
Wrong way
var adGroups = AdsApp.adGroups().get();
while (adGroups.hasNext()) {
var adGroup = adGroups.next();
if (adGroup.isPaused() == true) {
// Do something with paused ad group.
}
}
Don't traverse the entity hierarchy
If you want to get an entity's child entities or the entity's parent entity, don't traverse the entity hierarchy to get them.
To get child entities, use the child entity's collection at the level you want it.
Right way
// Get all ads.
var ads = AdsApp.ads().get();
while (ads.hasNext()) {
var ad = ads.next();
// Do something with ad.
}
Or, if you want ads from a specific campaign:
// Get all ads in the campaign, 'mycampaign'.
var ads = AdsApp.ads()
.withCondition("CampaignName = 'mycampaign'")
.get();
while (ads.hasNext()) {
var ad = ads.next();
// Do something with ad.
}
Or, getting a campaign's ads if you have the campaign object:
// Get all ads in the campaign.
var ads = campaign.ads().get();
while (ads.hasNext()) {
var ad = ads.next();
// Do something with ad.
}
Wrong way
var campaigns = AdsApp.campaigns().get();
while (campaigns.hasNext()) {
var adGroups = campaigns.next().adGroups().get();
while (adGroups.hasNext()) {
var ads = adGroups.next().ads().get();
while (ads.hasNext()) {
var ad = ads.next();
// Do something with ad.
}
}
}
The same applies if you want to get an entity's parent. Instead of traversing the hierarchy to get the parent, use the child entity's parent accessor method.
Right way
// Get all ads.
var ads = AdsApp.ads()
.withCondition('Clicks > 5')
.forDateRange('LAST_7_DAYS')
.get();
while (ads.hasNext()) {
var ad = ads.next();
// Do something with campaign and adGroup.
var adGroup = ad.adGroup();
var campaign = ad.campaign();
}
Wrong way
var campaigns = AdsApp.campaigns().get();
while (campaigns.hasNext()) {
var campaign = campaigns.next();
var adGroups = campaign.adGroups().get();
while (adGroups.hasNext()) {
var adGroup = adGroups.next();
var ads = adGroup.ads().get();
while (ads.hasNext()) {
var ad = ads.next();
if ('<some condition is met>') {
// Do something with campaign and adGroup.
}
}
}
}
Use entity IDs when possible
Using IDs to filter entities provides the best performance.
This
var adGroups = AdsApp.adGroups()
.withIds(["123456"])
.get();
while (adGroups.hasNext()) {
var adGroup = adGroups.next();
// Do something with adGroup.
}
Provides better performance than this
var adGroups = AdsApp.adGroups()
.withCondition("Name = 'myadgroup'")
.get();
while (adGroups.hasNext()) {
var adGroup = adGroups.next();
// Do something with adGroup.
}
Avoid tight loops with selectors and an unnecessary number of gets
Avoid loops with get requests that get a single entity. For example, let's say you run a keyword performance report and want to update keywords in the report. Instead of getting a row from the report, getting the keyword, and then updating it, you should create a list of the keyword IDs as you loop through each row in the report. Then, pass the list of IDs to the selector to get all the keywords in a single get request. You can then iterator through the list of keywords and update them.
Right way
var report = AdsApp.report('<report query goes here>');
var rows = report.rows();
var idLists = []; // an array where each element contains an array of IDs.
var idList = []; // array of IDs that's limited to maxCount.
var maxCount = 10000;
while (rows.hasNext()) {
var row = rows.next();
if (idList.length < maxCount) {
idList.push(row['id']);
}
else {
idLists.push(idList);
idList = [];
}
}
for (idList of idLists) {
var keywords = AdsApp.keywords()
.withIds(idList)
.get();
while (keywords.hasNext()) {
var keyword = keywords.next();
// update the keyword
}
}
Wrong way
var report = AdsApp.report('<report query goes here>');
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var keyword = AdsApp.keywords()
.withIds([row['id']])
.get()
.next();
// update the keyword
}
Include the forDateRange method only if you plan to call the entity's getStats method
Calling a selector's forDateRange method causes the selector to get the entity's performance data. Getting an entity's performance data is expensive, so only get it if you plan to call the entity's getStats method and use the data.
The date range that you specify for an entity does not apply to the parent or child entities that you access from that entity. For example, if you get an ad group and then get its parent campaign and try to access the campaign's performance metrics, the call fails.
The campaignStats.getReturnOnAdSpend() call in the following example fails because the date range applies to the ad group and not the campaign.
var myAdGroups = AdsApp.adGroups().
.withCondition("CampaignName CONTAINS 'gen'")
.forDateRange("LAST_7_DAYS")
.get();
while (myAdGroups.hasNext()) {
var adGroup = myAdGroups.next();
var campaign = adGroup.getCampaign();
var campaignStats = campaign.getStats();
var campaignROAS = campaignStats.getReturnOnAdSpend();
}
For this to work, you need to create a selector for the campaign.
var myAdGroups = AdsApp.adGroups().
.withCondition("CampaignName CONTAINS 'gen'")
.forDateRange("LAST_7_DAYS")
.get();
while (myAdGroups.hasNext()) {
var adGroup = myAdGroups.next();
var campaign = AdsApp.campaigns()
.withIds([adGroup.getCampaign().getId()])
.forDateRange("LAST_7_DAYS")
.get()
.next();
var campaignStats = campaign.getStats();
var campaignROAS = campaignStats.getReturnOnAdSpend();
}
Don't change an entity's property that's used as a condition in the selector
Iterators reduce memory pressure by loading only a single item at a time rather than the entire set of items. Because of this, changing a property that you used as a condition in the selector can cause unexpected behavior.
Right way
var adGroups = [];
var iterator = AdsApp.adGroups()
.withCondition('Status = ENABLED')
.get();
while (iterator.hasNext()) {
adGroups.push(iterator.next());
}
for (var adGroup of adGroups) {
adGroup.pause();
}
Wrong way
var adGroups = AdsApp.adGroups()
.withCondition('Status = ENABLED')
.get();
while (adGroups.hasNext()) {
var adGroup = adGroups.next();
adGroup.pause();
}
Batching updates
In order to improve performance, Scripts processes build requests in batches. If you call a build request's operation method, it forces Scripts to process the queued build requests immediately, negating any performance gains. If you're creating more than one entity, don't execute the operation methods in the same loop that you use to build the entity. This leads to poor performance because only one entity at a time is processed. Instead, create an array of the operations and process them after the build loop.
Right way
// An array to hold the operations, so you
// can process them after all the entities are queued.
var operations = [];
// Create all the new entities.
for (var i = 0; i < keywords.length; i++) {
var keywordOperation = AdsApp.adGroups().get().next()
.newKeywordBuilder()
.withText(keywords[i])
.build();
operations.push(keywordOperation);
}
// Now call the operation method so the build requests
// get processed in batches.
for (var i = 0; i < operations.length; i++) {
var newKeyword = operations[i].getResult();
}
Wrong way
for (var i = 0; i < keywords.length; i++) {
var keywordOperation = AdsApp.adGroups().get().next() // Get the first ad group
.newKeywordBuilder() // Add the keyword to the ad group
.withText(keywords[i])
.build();
// Don't get results in the same loop that creates
// the entity because Scripts then only processes one
// entity at a time.
var newKeyword = keywordOperation.getResult();
}
The same is true if you update an entity and then get the same property you updated. Don't do this:
var bidAmount = 1.2;
while (keywords.hasNext()) {
var keyword = keywords.next();
keyword.bidding().setCpc(bidAmount);
if (keyword.bidding().getCpc() != bidAmount) {
Logger.log(`Failed to update bid amount for keyword, ${keyword.getText()} (${keyword.getId()})`);
}
}
Use the yield keyword when getting large sets of entities
Retrieving a large number of entities and loading them into a single list that you process in a loop has a couple of disadvantages:
Depending on the size of the request, it may take n number of back-end requests to fetch all the entities before the loop starts. If you don't process them all, then the time and computing power used to get the unprocessed entities is wasted. For example, if you retrieve 10K keywords and the loop breaks after processing only 2K keywords, the time and computing power used to get the remaining 8K keywords is wasted.
Creating the list requires more memory to hold all the entities at the same time.
To address these issues, use the yield keyword, which allows your script to fetch entities on demand, or, in some sense, "lazily" fetch them only when they are needed. This means your script isn't making more calls than it needs at the moment and not passing around large lists of objects.
This example includes logging to illustrate the flow of control when using the yield keyword.
function main() {
const keywords = getKeywords();
//@ts-ignore <-- suppresses iterator error
for (const keyword of keywords) {
Logger.log("in for loop\n\n");
}
}
// Note that you must use the yield keyword in a generator function - see the
// '*' at the end of the function keyword.
function* getKeywords() {
const keywords = AdsApp.keywords()
.withCondition("Status = ENABLED")
.withCondition("CombinedApprovalStatus = APPROVED")
.withLimit(10)
.get();
Logger.log(`total keywords in account: ${keywords.totalNumEntities()} \n\n`);
while (keywords.hasNext()) {
Logger.log("before next()\n\n");
yield keywords.next();
Logger.log("after next\n\n");
}
}
Calling pattern to avoid entity limits
There's a limit to the number of entities that Scripts can return for an account. If the request would return more than this limit, Scripts throws an error with the message, There are too many entities. The following example shows the calling pattern you should use when getting large numbers of entities. The example tries to fetch all keywords at the account level first. If that fails, it tries to make multiple calls to fetch keywords by campaign, since there are typically fewer entities at the campaign level. You can generally continue this pattern down to the ad group level if needed.
function* getEntities() {
const applyConditions = _ => _
.withCondition('CampaignStatus = ENABLED')
.withCondition('AdGroupStatus = ENABLED')
.withCondition('Status = ENABLED')
.withCondition("CombinedApprovalStatus = DISAPPROVED");
try {
// Get the account's keywords.
const keywords = applyConditions(AdsApp.keywords()).get();
while (keywords.hasNext()) {
yield keywords.next();
}
} catch (e) {
if (!e.message.startsWith('There are too many entities')) {
throw e;
}
// If there are too many keywords at the account level,
// get keywords by campaigns under the account.
const campaigns = AdsApp.campaigns().get();
while (campaigns.hasNext()) {
const campaign = campaigns.next();
const keywords = applyConditions(campaign.keywords()).get();
while (keywords.hasNext()) {
yield keywords.next();
}
}
}
}
This module focuses on educating developers on how to perform common techniques in regard to automating business processes using client scripting, along with explaining the context of when these scenarios might be applied.