效能與執行限制
寫了很多 Google Apps Script,會不會拖慢彼此?單次能跑多久?這頁先回答第一個問題,再列出官方的執行限制與每日配額,最後給幾個實務上最有效的優化手法。
專案很多會有效能問題嗎
專案數量本身不是瓶頸。 每次執行都在 Google 雲端獨立跑,不佔你電腦的資源,也不會卡住試算表畫面。官方配額表裡也沒有「專案數量」這一項。真正會撞牆的是同一個帳號共用的總量:
| 你擔心的 | 實際情況 |
|---|---|
| 專案多,整體會變慢 | 不會。執行彼此獨立;慢通常來自單支程式的寫法,不是專案數 |
一份表掛很多 onEdit |
每次編輯都會啟動一次執行,不會卡住你打字,但每次都吃配額與並行數。多人同時編輯可能撞到並行上限 |
| 掛了很多時間觸發 | 每人每專案最多 20 個;更重要的是帳號所有觸發的總執行時間每天有上限(一般帳號 90 分鐘、Workspace 6 小時) |
| 很多腳本一起寄信、打 API | 寄信、UrlFetchApp 的每日次數是整個帳號共用,某支腳本跑失控會害其他腳本一起停擺 |
| 程式跑得慢 | 最常見原因是迴圈裡一格一格呼叫 SpreadsheetApp,每次呼叫都是一趟往返 |
還有一個實務觀察(官方文件沒寫數字):Web App 久沒人呼叫後的第一個請求會比較慢。需要即時回應的用途,例如聊天機器人,要預期偶爾有一次明顯的延遲。
執行限制
下表節錄自官方 配額與限制,數字會調整,上線前請再對一次。
| 項目 | 上限 |
|---|---|
| 單次執行時間 | 6 分鐘 |
| 簡易觸發、自訂函式、Workspace 外掛 | 30 秒 |
| 同時執行數 | 每位使用者 30、每支腳本 1,000 |
| 每人每專案的觸發條件數 | 20 |
UrlFetchApp 單次回應/POST 內容 |
各 50 MB |
UrlFetchApp 網址長度 |
2 KB |
| 屬性(Properties)單一值/總容量 | 9 KB/500 KB |
| 每封郵件收件人數 | 50 |
| 每封郵件附件總大小 | 25 MB |
| 專案版本數 | 200 |
超過時間會怎樣: 執行被直接終止,不會自動接著跑,也不會替你收拾半途的工作。長工作必須自己切段,見下文。
每日配額
| 項目 | 一般 Google 帳號 | Google Workspace |
|---|---|---|
| 寄出的收件人數 | 100 | 1,500 |
UrlFetchApp 呼叫次數 |
20,000 | 100,000 |
| 所有觸發的總執行時間 | 90 分鐘 | 6 小時 |
| 屬性讀寫次數 | 50,000 | 500,000 |
| 建立日曆事件 | 5,000 | 10,000 |
| 建立文件 | 250 | 1,500 |
| 建立試算表 | 250 | 3,200 |
配額用完之後,該功能會丟例外,通常隔天重置。監控最直接的方法:把重要腳本的失敗寫進 執行紀錄與錯誤工作表。
怎麼讓腳本又快又穩
批次讀寫,不要逐格呼叫
// 慢:每一列都往返一次
for (let i = 2; i <= last; i++) {
const v = sheet.getRange(i, 1).getValue();
sheet.getRange(i, 2).setValue(v * 2);
}
// 快:讀一次、算完、寫一次
const values = sheet.getRange(2, 1, last - 1, 1).getValues();
const out = values.map(([v]) => [v * 2]);
sheet.getRange(2, 2, out.length, 1).setValues(out);
用快取避免重複打 API
CacheService 是暫存區:值最大 100 KB、最多 1,000 項、預設保留 10 分鐘、最長 6 小時。適合存「不常變、又常被查」的結果,例如匯率、設定表。
function getRate() {
const cache = CacheService.getScriptCache();
const hit = cache.get("rate");
if (hit) return Number(hit);
const res = UrlFetchApp.fetch("https://api.example.com/rate");
const rate = JSON.parse(res.getContentText()).rate;
cache.put("rate", String(rate), 600); // 存 10 分鐘
return rate;
}
多個請求用 fetchAll 一次送
UrlFetchApp.fetchAll(requests) 讓多個請求並行送出,比在迴圈裡逐一 fetch 快很多,也較不容易撞到 6 分鐘。
同時寫入用 LockService 排隊
多人同時觸發同一支腳本(例如很多人同時編輯、機器人同時收到訊息),若做的是「先讀再改同一格」,會互相覆蓋。用鎖讓它們一個一個做:
function safeIncrement() {
const lock = LockService.getScriptLock();
lock.waitLock(10000); // 最多等 10 秒,逾時會丟例外
try {
const cell = SpreadsheetApp.getActive().getRange("Counter!A1");
cell.setValue(Number(cell.getValue()) + 1);
} finally {
lock.releaseLock();
}
}
長工作切段續跑
大表逐列處理可能超過 6 分鐘。做法:跑到接近上限就把進度存進 PropertiesService,排一個一分鐘後的一次性觸發接著跑。
const MAX_MS = 4.5 * 60 * 1000; // 留餘裕,別跑滿 6 分鐘
function processAll() {
const start = Date.now();
const props = PropertiesService.getScriptProperties();
const sheet = SpreadsheetApp.getActive().getSheetByName("待處理");
const last = sheet.getLastRow();
let row = Number(props.getProperty("nextRow") || 2);
clearResumeTriggers();
while (row <= last) {
if (Date.now() - start > MAX_MS) {
props.setProperty("nextRow", String(row));
ScriptApp.newTrigger("resumeJob").timeBased().after(60 * 1000).create();
return;
}
handleRow(sheet, row); // 你自己的單列處理
row++;
}
props.deleteProperty("nextRow"); // 全部做完,清掉進度
}
function resumeJob() {
processAll();
}
function clearResumeTriggers() {
ScriptApp.getProjectTriggers()
.filter((t) => t.getHandlerFunction() === "resumeJob")
.forEach((t) => ScriptApp.deleteTrigger(t));
}
先清掉舊的接力觸發再建新的,才不會像 觸發條件 提醒的那樣疊出一堆。
簡易觸發要早退
onEdit 每次編輯都會跑。第一行就判斷工作表、欄位,不相關就 return。這是零成本、效果最大的優化。
什麼時候該離開 GAS
| 需求 | 為什麼 GAS 吃力 | 常見替代 |
|---|---|---|
| 高流量、要穩定低延遲 | 有冷啟動、有並行與每日配額 | Google Cloud 的 Cloud Run 等 |
| 單次運算超過 6 分鐘 | 會被強制終止 | 排程的雲端工作、佇列 |
| 需要真正的資料庫、交易 | 試算表不是資料庫 | Cloud SQL、Firestore 等 |
| 要讀 HTTP 標頭(如驗證 Webhook 簽章) | Web App 拿不到請求標頭 | 前面放一層中繼,見 LINE Bot 上線注意 |
多數辦公室自動化離這些門檻很遠。先寫,撞到再換不遲。