Быстрые и экономные скрипты
У скрипта есть 30 секунд на запуск и 300 минут работы в сутки на все скрипты владельца таблицы. Обычно этого хватает с большим запасом, если соблюдать несколько правил. Все цифры собраны в статье Лимиты.
Главное правило: блоками, а не по ячейке
Скрипт обращается к таблице по сети, и за запуск можно сделать не больше 100 обращений. Больше всего их тратят чтения:
getValue,getValuesи другие чтения — одно обращение на каждые 1000 строк;getLastRow,getLastColumnиgetDataRange— по два обращения;- записи (
setValue,setValues,appendRow) копятся: идущие подряд записи значений уходят одним обращением, добавление строк и очистка — отдельными. Любое чтение сначала отправляет накопленные записи; - первое обращение к листу по названию загружает список листов, это ещё одно обращение.
Поэтому:
- Прочитайте весь лист одной командой.
- Обработайте данные в коде.
- Запишите результат одной командой.
Медленно: каждый шаг цикла читает и пишет вперемешку, тратит около пяти обращений, и примерно на двадцатой строке скрипт остановится с ошибкой «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в цикле. - Оформляю диапазон целиком, раскраску по значению делаю правилом подсветки.
- Большие объёмы обрабатываю частями с отметкой в «Памяти».
- Выхожу из функции сразу, если событие меня не касается.
- Триггер «При изменении» ограничен нужным листом и диапазоном.
- Расписание не чаще, чем нужно задаче.
- Запросы к сервисам и ИИ собираю в один.