Перейти к основному содержимому

Быстрые и экономные скрипты

У скрипта есть 30 секунд на запуск и 300 минут работы в сутки на все скрипты владельца таблицы. Обычно этого хватает с большим запасом, если соблюдать несколько правил. Все цифры собраны в статье Лимиты.

Главное правило: блоками, а не по ячейке​

Скрипт обращается к таблице по сети, и за запуск можно сделать не больше 100 обращений. Больше всего их тратят чтения:

  • getValue, getValues и другие чтения — одно обращение на каждые 1000 строк;
  • getLastRow, getLastColumn и getDataRange — по два обращения;
  • записи (setValue, setValues, appendRow) копятся: идущие подряд записи значений уходят одним обращением, добавление строк и очистка — отдельными. Любое чтение сначала отправляет накопленные записи;
  • первое обращение к листу по названию загружает список листов, это ещё одно обращение.

Поэтому:

  1. Прочитайте весь лист одной командой.
  2. Обработайте данные в коде.
  3. Запишите результат одной командой.

Медленно: каждый шаг цикла читает и пишет вперемешку, тратит около пяти обращений, и примерно на двадцатой строке скрипт остановится с ошибкой «Too many Coverse API calls in one run»:

function main() {
const sheet = Coverse.table.getSheet("Заказы");
for (let row = 2; row <= sheet.getLastRow(); row++) {
const price = sheet.getRange(row, 3).getValue();
const count = sheet.getRange(row, 4).getValue();
sheet.getRange(row, 5).setValue(price * count);
}
}

Быстро, несколько обращений для листа в десятки тысяч строк:

function main() {
const sheet = Coverse.table.getSheet("Заказы");
const last = sheet.getLastRow();
if (last < 2) return;

const rows = sheet.getRange(2, 3, last - 1, 2).getValues();
const totals = rows.map((row) => {
const total = Number(row[0]) * Number(row[1]);
return [Number.isFinite(total) && row[0] !== "" && row[1] !== "" ? total : ""];
});
sheet.getRange(2, 5, totals.length, 1).setValues(totals);
}

То же для добавления строк: соберите их в массив и запишите одной командой appendRows(rows). Так код короче, и чтение между добавлениями не разобьёт запись на отдельные обращения.

Если для расчёта подходит формула, запишите её один раз через setFormula или setFormulas, и дальше таблица будет считать сама, без запусков скрипта.

Оформление — диапазоном целиком​

  • Оформляйте сразу весь блок: sheet.getRange("A1:G1").setBackground("#fff2cc").setFontWeight("bold"), а не каждую ячейку в цикле. За запуск доступно 300 вызовов оформления.
  • Раскраску по значению не нужно пересчитывать скриптом при каждом изменении. Один раз создайте правило подсветки через setConditionalFormat, дальше таблица красит сама, в том числе новые строки. Пример — в Готовых сценариях.
  • Чтобы новые строки выглядели как старые, используйте copyFormatTo на весь блок.

Большой объём — частями​

Если данных больше, чем успевается за 30 секунд, обрабатывайте их частями и запоминайте в Памяти, где остановились. Запустите такой скрипт по расписанию: каждый запуск продолжит с места, где закончил предыдущий.

const BATCH = 500;

function main() {
const sheet = Coverse.table.getSheet("Клиенты");
const last = sheet.getLastRow();
const start = Number(Properties.script.get("nextRow")) || 2;

if (start > last) {
console.log("Всё обработано");
return;
}

const count = Math.min(BATCH, last - start + 1);
const rows = sheet.getRange(start, 1, count, 6).getValues();
const result = rows.map((row) => [String(row[2]).trim().toLowerCase()]);
sheet.getRange(start, 7, count, 1).setValues(result);
Coverse.table.flush();

Properties.script.set("nextRow", start + count);
console.log("Обработаны строки", start, "–", start + count - 1);
}

Coverse.table.flush() отправляет запись в таблицу сразу. Если она не удалась, скрипт остановится до того, как сдвинет отметку, и следующий запуск повторит эту часть. Без flush запись ушла бы только в конце запуска, а «Память» сохраняется при любом исходе.

Когда нужно начать заново, удалите ключ nextRow на вкладке «Память» или командой Properties.script.delete("nextRow").

Выходите, когда делать нечего​

Запуск, который сразу заканчивается, почти не тратит минут. Проверяйте условия в начале функции:

function onEdit(event) {
if (!event) return;

const relevant = event.changes.filter((change) => change.sheet === "Заказы" && change.column === 5);
if (relevant.length === 0) return;

const sheet = Coverse.table.getSheet("Заказы");
for (const change of relevant) {
if (change.value === "Оплачен") {
sheet.getRange(change.row, 6).setValue(Utilities.today());
}
}
}

Для загрузки из внешнего сервиса запоминайте отметку последней загрузки и запрашивайте только новое, см. пример Загрузка новых сделок из CRM.

Настройте триггеры так, чтобы лишних запусков не было​

  • Ограничьте триггер «При изменении» листом и диапазоном в его настройках. Тогда правки в других местах вообще не запускают скрипт.
  • Выбирайте расписание по задаче. Отчёт раз в день не нужно запускать каждые 15 минут: 96 запусков в сутки против одного.
  • Разнесите расписания разных скриптов по времени. Одновременно выполняется до 3 скриптов одного владельца, остальные ждут или пропускаются.
  • Правки самого скрипта не запускают onEdit повторно, поэтому писать в ту же таблицу из onEdit безопасно, бесконечного цикла не будет.
  • Если обработка нужна только по кнопке, не ставьте триггер, а добавьте пункт в меню «Макросы».

Внешние сервисы​

  • Время ожидания ответа входит во время запуска. Медленный сервис съедает и 30 секунд запуска, и норму минут. Задавайте timeout и проверяйте response.ok.
  • Запрашивайте только нужное: фильтр по дате, номер страницы, ограничение числа записей — так, как это поддерживает сервис.
  • За запуск доступно 50 внешних запросов вместе с Telegram.send. Если нужно отправить много сообщений, соберите их в одно.
  • Не ждите ответа сервиса в цикле с Utilities.sleep: пауза тоже тратит время запуска. Лучше запустить скрипт снова по расписанию.

ИИ-кредиты​

Каждый вызов CoverseAI.ask стоит 7 ИИ-кредитов, за запуск доступно 5 вызовов. Отправляйте строки таблицей в одном запросе через data и просите ответ списком или в JSON, а не вызывайте ИИ на каждую строку. В одном запросе — заголовок и до 200 строк, всего до 20 000 знаков, поэтому длинные тексты отправляйте порциями поменьше:

const rows = Coverse.table.getSheet("Отзывы").getRange("A1:B41").getValues();
const tones = CoverseAI.ask(
"Для каждой строки определи тон отзыва: позитивный, нейтральный или негативный. Верни JSON-массив с тоном для каждой строки в том же порядке.",
{ data: rows, json: true },
);
console.log(tones);

Подробнее: ИИ в скриптах.

Как понять, на что уходит время​

  • В журнале запусков у каждого запуска указана длительность. Сравните с 30 секундами.
  • Поставьте console.log с временем до и после подозрительного места:
const started = Date.now();
const rows = sheet.getDataRange().getValues();
console.log("Чтение заняло", Date.now() - started, "мс");
  • Расход нормы за сегодня виден в панели скриптов: «Сегодня: N мин из 5 ч».

Короткий чек-лист​

  • Читаю и пишу данные блоками, без getValue и setValue в цикле.
  • Оформляю диапазон целиком, раскраску по значению делаю правилом подсветки.
  • Большие объёмы обрабатываю частями с отметкой в «Памяти».
  • Выхожу из функции сразу, если событие меня не касается.
  • Триггер «При изменении» ограничен нужным листом и диапазоном.
  • Расписание не чаще, чем нужно задаче.
  • Запросы к сервисам и ИИ собираю в один.

Что дальше​