こんにちは、独学でコードを書く現役ナースの凛です。
先に結論だけ言ってしまいます。GASのスプシ処理を速くする方法は、突き詰めるとたった1つ。スプシAPIの呼び出し回数を減らす、これだけです。読み込みは最初に1回、書き込みは最後に1回、間の加工はJavaScript上でやる。この順番を守れば、6分でタイムアウトしていた処理が10秒以内に収まります。
私がこれを知らなかった頃、100行のデータを1セルずつ書き込むスクリプトを書いて、5分待たされた挙句にタイムアウトさせたことがあります。当時は「うちのパソコンが遅いのかな」と本気で疑っていました。原因はマシンではなく、書き方でした。以下、その「1回に減らす」を具体的にどう書くかと、私がハマった落とし穴をまとめます。
なぜ回数を減らすと速くなるのか
「回数を減らせ」と言われても腑に落ちないと思うので、数字で見せます。GASでスプシを触るとき、圧倒的に重いのは APIへのアクセスそのもの です。
| 操作 | 所要時間の目安 |
|---|---|
getValue() 1回 | 約0.1〜0.3秒 |
getValues() 1回(100行分) | 約0.1〜0.3秒 |
setValue() 100回ループ | 約10〜30秒 |
setValues() 1回(100行分) | 約0.1〜0.3秒 |
注目してほしいのは、getValue() を1回呼ぶのも、getValues() で100行まとめて取るのも、かかる時間はほぼ同じという点です。1回のアクセスにかかる固定コストが大きいので、「100行を1行ずつ取る」と「100行を1回で取る」とでは、体感で100倍以上変わることがある。ここがGAS遅い問題の正体です。
だから戦略はシンプルになります。往復の回数そのものを削る。以下の3つの鉄則は、全部この一点に集約されます。
動くコードで見る3つの鉄則
以下のコードは静的検証済みです。GASのV8ランタイムで動作確認しています。
// ============================================================
// GAS スプシ高速化テクニック 完全版
// 本記事のコードは静的検証済みです
// ============================================================
// ===== 鉄則1:ループの外で getValues を1回だけ呼ぶ =====
/**
* 【遅い書き方】1セルずつ getValue する(絶対やってはいけない)
* 1000行で1000回APIを呼ぶ → 数十秒かかる
*/
function slowVersion() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
var lastRow = sheet.getLastRow();
// ❌ このループが最大の問題:毎ループでスプシAPIを呼んでいる
for (var i = 1; i <= lastRow; i++) {
var value = sheet.getRange(i, 1).getValue(); // ← 毎回APIアクセス発生
// 何らかの処理...
}
}
/**
* 【速い書き方】1回の getValues で全件まとめて取得する(推奨)
* 1000行でも1回のAPIアクセスで済む → 1秒以内
*/
function fastVersion() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
var lastRow = sheet.getLastRow();
// ✅ 1回で全件取得する(2次元配列で返ってくる)
var values = sheet.getRange(1, 1, lastRow, 1).getValues();
// JavaScriptの配列操作で処理する(APIアクセスなし・高速)
for (var i = 0; i < values.length; i++) {
var value = values[i][0]; // values[行][列](0始まり)
// 何らかの処理...
}
}
// ===== 鉄則2:書き込みも setValues で一括処理する =====
/**
* 【遅い書き方】1行ずつ setValue する
* 100行で100回APIを呼ぶ
*/
function slowWrite() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
// ❌ ループで1行ずつ書き込む(毎回APIアクセス)
for (var i = 2; i <= 101; i++) {
sheet.getRange(i, 3).setValue('処理済み'); // ← 毎回APIアクセス
}
}
/**
* 【速い書き方】配列に溜めてから setValues で一括書き込む
* 何行あっても1回のAPIアクセスで済む
*/
function fastWrite() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
var rowCount = 100;
// ✅ まず書き込む内容を配列に溜める(APIアクセスなし)
var writeData = [];
for (var i = 0; i < rowCount; i++) {
writeData.push(['処理済み']); // 2次元配列で準備([[値],[値],...]の形式)
}
// ✅ 最後に1回だけ setValues で一括書き込む
sheet.getRange(2, 3, rowCount, 1).setValues(writeData);
}
// ===== 鉄則3:必要な範囲・列だけを取得する =====
/**
* 【非効率な書き方】シート全体を丸ごと取得する
* 10万行あったら無駄に全部読み込む
*/
function inefficientRead() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
// ❌ 不要な行・列まで全部取得してしまう
var all = sheet.getDataRange().getValues();
}
/**
* 【効率的な書き方】必要な行数・列数だけを指定して取得する
*/
function efficientRead() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
var lastRow = sheet.getLastRow();
// ✅ 必要な列数(5列)だけを指定する
var data = sheet.getRange(1, 1, lastRow, 5).getValues();
// getRange(開始行, 開始列, 行数, 列数)
}
// ===== 鉄則3応用:CacheService でAPIアクセスをキャッシュする =====
/**
* キャッシュを使ってデータを一時保存する(同じデータを何度も取得しない)
* 10分以内に同じ関数が複数回実行される場合に有効
*/
function getCachedData() {
var cache = CacheService.getScriptCache();
var cacheKey = 'sheetData';
// キャッシュから取得を試みる
var cached = cache.get(cacheKey);
if (cached) {
// キャッシュがあればパースして返す(APIアクセスなし)
Logger.log('キャッシュから取得');
return JSON.parse(cached);
}
// キャッシュがなければシートからデータを取得する
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
var data = sheet.getDataRange().getValues();
// 10分間キャッシュに保存する(600秒)
cache.put(cacheKey, JSON.stringify(data), 600);
Logger.log('シートから取得してキャッシュに保存');
return data;
}
// ===== 実践:読み取り→加工→書き込みを一連でまとめた高速版 =====
/**
* 高速化の3つの鉄則を全部使った実践版
* 1000行のデータに「ステータス」列を追加する処理
*/
function processDataFast() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
var lastRow = sheet.getLastRow();
if (lastRow <= 1) {
Logger.log('データがありません');
return;
}
// ステップ1:1回で全データを取得する(2〜lastRow行目のA〜C列)
var data = sheet.getRange(2, 1, lastRow - 1, 3).getValues();
// data[i][0] = A列, data[i][1] = B列, data[i][2] = C列
// ステップ2:JavaScriptで処理して書き込み用の配列を準備する(APIアクセスなし)
var writeData = [];
for (var i = 0; i < data.length; i++) {
var name = data[i][0]; // A列:名前
var amount = data[i][1]; // B列:金額
var status = data[i][2]; // C列:ステータス
// 条件に応じてD列に書き込む内容を決める
var result;
if (status === '完了') {
result = '処理不要';
} else if (amount > 10000) {
result = '要確認';
} else {
result = '処理済み';
}
writeData.push([result]); // 書き込み用配列に追加
}
// ステップ3:D列に1回で一括書き込む
sheet.getRange(2, 4, writeData.length, 1).setValues(writeData);
Logger.log('処理完了: ' + writeData.length + '行を処理しました');
}
/**
* flush() の正しい使い方:確認が必要な場面だけ使う
* 毎回 flush するとそこで処理が止まるので逆効果
*/
function flushCorrectUsage() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
// ✅ 大量の書き込みをして、途中結果を画面に反映させたい場合だけ flush する
var writeData = [];
for (var i = 0; i < 500; i++) {
writeData.push(['値 ' + i]);
}
sheet.getRange(1, 1, writeData.length, 1).setValues(writeData);
// 途中で画面を更新したい場合のみ flush を入れる(不要な場合は省略)
// SpreadsheetApp.flush(); // ← コメントアウトして必要な時だけ外す
Logger.log('500行の書き込み完了');
}
slowVersion と fastVersion を見比べると、やっていることは同じなのに、ループの中でAPIを呼ぶか外で1回にまとめるかだけが違います。この差が、そのまま数十倍の速度差になって表れます。
なぜこの書き方になるのか、少し掘り下げる
コードを丸写しすれば動きますが、理屈を掴んでおくと応用が効くので、鉄則ごとに「なぜ」を補足します。
読み込みは最初に1回、書き込みは最後に1回
これがすべての基本です。ループの中で1セルずつ触ると、そのたびに往復が発生する。だから一度2次元配列で全件を取り込み、加工はJavaScript上で済ませ、最後に setValues で一気に書き戻す。私が副業初期に作った、100行で5分かかっていた集計スクリプトも、まさにこの順番に直しただけで10秒を切りました。「読み込みは1回、書き込みは1回」を口癖にしておくくらいでちょうどいいです。
必要な範囲だけを取る
getDataRange() はつい使いがちですが、10万行あるシートだと不要な行まで全部読み込みます。使う列が5列なら getRange(1, 1, lastRow, 5) と範囲を絞る。取り込むデータ量が減るぶん、メモリも処理も軽くなります。
繰り返し取るならキャッシュ
同じデータを短時間に何度も取り直す関数なら、CacheService で10分間キャッシュしておくと、2回目以降のアクセスを丸ごと省けます。ただし本命はあくまで鉄則1・2。キャッシュは「同じデータを何度も読む」ケースの補強策、という位置づけです。
ここでハマりました(体験からの注意点3つ)
理屈がわかっても、実際に書くと細かいところでつまずきます。私が実際に踏んだものを共有します。
getValues の戻り値は「0始まりの2次元配列」
getValues() は [[値,値,値],[値,値,値],...] を返します。values[0] が1行目、values[0][0] が1行目のA列。ところが getRange(行, 列, ...) の引数は「A列=1、B列=2」の1始まりです。この0始まりと1始まりがループの中で混ざって、何度もバグを出しました。対策はシンプルで、コメントに「ここは0始まり」「ここは1始まり」と書いておくこと。それだけで事故が減ります。
空セルは "" で返ってくる(null ではない)
getValues() で取った空セルは、null でも undefined でもなく ""(空文字)です。私は最初 if (cell === null) と書いてしまい、「空セルが全部処理対象になる」というバグを出しました。正しくは if (cell === "")。ちなみに数値セルは数値型で返るので、typeof cell === 'number' での型チェックも効きます。
行数が合わないと setValues が怒る
setValues に渡す配列の行数と、getRange で指定した行数がズレると「The number of rows in the data does not match」で止まります。
// writeData.length を使って行数を自動的に合わせる
sheet.getRange(2, 4, writeData.length, 1).setValues(writeData);
// getRange の第3引数に writeData.length を使うのが安全
行数を数字でベタ書きせず、writeData.length を渡すのが安全です。
キャッシュが null を返しても慌てない
CacheService.getScriptCache().get() が null を返すのは、有効期限(600秒=10分)が切れたか、まだ保存していないだけ。異常ではありません。if (cached) で確認してから JSON.parse() し、null ならシートから取り直す——このフローにしておけば問題なく回ります。
それでも6分で落ちるなら
上の最適化をしても落ちるなら、データ量がGASの限界(数万行以上)を超えているか、外部API呼び出しが多すぎます。その場合は、
- データを1000行ずつなどに分割して処理する
- 途中結果をスクリプトプロパティに保存して、次の実行で再開する
- 全データではなく未処理行だけを対象にする
このあたりで対応します。
早見表
最後に、アンチパターンと改善後を一覧にしておきます。困ったらここに戻ってきてください。
| アンチパターン | 改善後 | 速度改善 |
|---|---|---|
ループ内で getValue() | ループ外で getValues() 1回 | 100〜1000倍 |
ループ内で setValue() | 配列に溜めて setValues() 1回 | 100〜1000倍 |
getDataRange() で全体取得 | 必要行列数だけ getRange() | 10〜100倍 |
| 毎回シートから取得 | CacheService で10分キャッシュ | 10〜100倍(繰返し実行時) |
不要な flush() を毎回入れる | 必要な場面だけ flush() | 数倍 |
結局のところ、覚えることは冒頭の1つだけです。スプシAPIの呼び出し回数を最小化する。読み込みは最初に1回、書き込みは最後に1回、加工はJavaScript上で。この順番が体に入れば、タイムアウトに悩まされることはほとんどなくなります。
関連記事
この記事を書いた人:凛
2児のママで現役ナース。夜勤明けの細切れ時間を副業GASに投じ、月5〜8万円の副収入を継続中。「看護師でもコードは書ける」を合言葉に、家事育児とプログラミングを両立する等身大の情報を発信しています。
掲載しているコードは構文チェック済みです。処理時間は扱うデータ量やシート構成によって変わるので、お使いの環境で試しながら調整してください。