google.script.run が遅いときは、呼ぶ回数から減らす
Web アプリの画面が重いと感じたとき、サーバー側の処理を速くしようとしがちです。 ただ、google.script.run は1回呼ぶごとに往復の待ちが入ります。先に数えるのは、呼んでいる回数です。
画面からサーバーの関数を呼ぶと、ブラウザから Google のサーバーへ頼みに行きます。結果は、また往復して戻ってきます。サーバー側の関数が中で何もしなくても、この往復の分は待つことになります。
仮に1往復に1秒かかるとします。画面を開いたときに、メニュー・一覧・設定を別々に取りに行くと、それだけで3秒です。サーバー側をいくら速くしても、この3秒は縮みません。
呼び出しは非同期で、順番も決まっていない
google.script.run は値を直接返しません。結果は withSuccessHandler に渡したコールバックで受け取ります。呼んだ側のコードは、返事を待たずに次の行へ進みます。
2つを同時に呼んだとき、どちらが先に動くかはわかりません。ページを読み込むたびに変わることもあります。「設定を保存してから一覧を読み直す」処理を、2回の呼び出しに分けたとします。一覧のほうが先に動くと、保存前の内容が返ってきます。
同時に動ける呼び出しは10件までです。11件目は、どれかが終わるまで待たされます。ブラウザの側にも同じサーバーへの同時接続の上限があり、公式のガイドでは Firefox の6件が例に挙がっています。全部を同時に投げても、思ったほどは並びません。
起動時に要るものは、1回で取る
画面を開いたときに要るものを、サーバー側で1つの関数にまとめます。
// コード.gs
function getInitialData() {
const ss = SpreadsheetApp.openById(SHEET_ID); // SHEET_ID は自分の表の ID
return {
menu: readMenu(ss),
items: readItems(ss),
settings: readSettings(ss),
};
}
readMenu() などは、表を読んで配列やオブジェクトを返す自分の関数に置き換えてください。スプレッドシートは1回だけ開いて、それぞれの関数に渡しています。
画面の側は、この1回の結果で全部を描きます。
google.script.run
.withSuccessHandler((data) => {
renderMenu(data.menu);
renderItems(data.items);
renderSettings(data.settings);
})
.withFailureHandler((err) => showError(err.message))
.getInitialData();
render〜() と showError() は、画面を描く自分の関数です。3回の往復が1回になり、順番を気にする必要もなくなります。
保存も同じ考え方で組めます。保存したあとに一覧を読み直すなら、保存する関数の戻り値に新しい一覧を入れて返します。画面から2回呼ぶより、待ちが1往復ぶん減ります。
テンプレートに埋め込むと、ページ全体が待たされる
HTML をテンプレートとして返すと、<?= ?> の中でサーバーの関数を呼べます。往復が減るので速そうに見えますが、公式のベストプラクティスでは勧めていません。テンプレートの中のコードはページを読み込むときに1回だけ動き、終わるまでブラウザには何も送られないからです。
表を読む処理をテンプレートに入れると、その間は真っ白な画面のままになります。ページの枠だけ先に返して、データは google.script.run で取りに行けば、読み込み中の表示を出しておけます。スクリプトレットは、別ファイルの差し込みや固定の値くらいにとどめておくのが勧められている使い方です。
Promise で包んでおくと、組み立てやすい
コールバックのままだと、呼び出しを続けて書いたときに入れ子が深くなります。Promise で包んでおくと、await で順番に書けます。
function run(name, ...args) {
return new Promise((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
[name](...args);
});
}
// 使うとき
const data = await run('getInitialData');
name にはサーバー側の関数名を文字列で渡します。await を並べると、前の呼び出しが終わってから次が動きます。順番が決まるかわりに、待ちはそのぶん足し算になります。
サーバー側では、表を読む回数を減らす
往復を減らしたあとに残るのは、サーバーで処理にかかる時間です。ここで効くのは、スプレッドシートを触る回数です。
Apps Script の中の計算は、スプレッドシートなどほかのサービスを呼ぶより速く動きます。読み書きを交互に繰り返す書き方は遅くなります。1回で配列に読み込み、配列のまま処理し、1回で書き出す形にします。
| 遅くなる書き方 | 置き換え |
|---|---|
関数ごとに SpreadsheetApp.openById() を呼ぶ |
1回開いた ss を引数で渡す |
1行ずつ getValue() で読む |
getDataRange().getValues() で一度に読む |
ループの中で setValue() |
配列を作ってから setValues() を1回 |
| 同じシートを関数ごとに読み直す | 1回読んだ配列を使い回す |
たとえば readMenu() と readItems() が同じシートをそれぞれ読んでいると、同じ読み込みが2回走ります。どの関数がどのシートを読んでいるかを書き出してみると、重なっているところが見つかります。
変わっていなければ、前の結果を返す
読む回数を減らしきったら、そもそも読まずに済ませる手があります。CacheService に結果を置いておき、次の呼び出しではそれを返します。
function getInitialDataCached() {
const version = PropertiesService.getScriptProperties().getProperty('version') || '0';
const key = 'initial:' + version;
const cache = CacheService.getScriptCache();
const hit = cache.get(key);
if (hit) return JSON.parse(hit);
const data = getInitialData();
const text = JSON.stringify(data);
// 1つのキーに入るのは 100KB まで。length は文字数なので、バイト数で比べる
if (Utilities.newBlob(text).getBytes().length < 90 * 1024) cache.put(key, text, 300);
return data;
}
// 書き込む関数の最後で呼ぶ
function bumpVersion() {
const props = PropertiesService.getScriptProperties();
props.setProperty('version', String(Number(props.getProperty('version') || '0') + 1));
}
書き込むたびに bumpVersion() を呼ぶと、キーが変わるので古い控えは使われなくなります。300 は控えておく秒数です。
保存期間は1秒から21600秒(6時間)の間で指定でき、省くと600秒になります。指定した期間は目安で、キャッシュが多いと先に消えることもあります。消えても読み直すだけなので、困るのは速さだけです。
getScriptCache() は、そのスクリプトを使う全員で共通です。人によって中身が変わるデータを共通のキーで置くと、別の人の内容が返ってしまいます。そういうデータは、キーに人を見分ける値を入れておいてください。
どこから手をつけるか
画面を開いてから使えるようになるまでに、google.script.run を何回呼んでいるかを数えるのが先です。呼び出しを上の run() に通しておくと、数えるのは簡単です。中に console.log(name) を1行足せば、開いた直後に何が呼ばれているかがわかります。
回数が1〜2回まで減っていて、それでも遅いなら、サーバー側で表を読んでいる回数を見ます。エディタの「実行数」の画面で、関数ごとにかかった時間を確かめられます。
書かなくても動く状態のコードを、下のツールで配っています。
社内wiki手順書や議事録を1か所にまとめます。契約も月額もなく、データは自分の Google ドライブの中だけに置かれます。