はじめに
Google Apps Script (GAS) でWebアプリを開発していた際、「サーバー側の処理は成功しているのに、location.reload()で画面を更新すると真っ白になる」という問題に遭遇しました。
この記事では、その原因とPromise/async-awaitを活用した解決策を自身のメモとして記録します。
🛑 発生した問題
症状
サーバー処理完了後にlocation.reload()を実行すると、画面が真っ白になり、ブラウザの手動リロードが必要になる。
❌ 問題のあるコード
google.script.run
.withSuccessHandler(msg => {
showMessage(msg + ' 処理が完了しました!');
setTimeout(() => {
location.reload(); // ← これで画面が真っ白になる
}, 2000);
})
.serverFunction();
環境
HTMLファイルのサイズ: 約6,000行、200KB以上
データ量: Base64エンコードされた大量のJSONデータをテンプレートに埋め込み
ブラウザ: Chrome(最新版)
🔍 試したこと
1. setTimeoutで遅延させる ▶️ ❌ 効果なし
```js
setTimeout(() => {
location.reload();
}, 3000); // 3秒待っても真っ白
```
→ 結果: 変わらず真っ白になる
2. 二段階setTimeoutで十分な時間を確保 ➡️ ❌ 効果なし
```js
setTimeout(() => {
setTimeout(() => {
location.reload();
}, 500);
}, 100);
```
→ 結果: これでも真っ白になる
💡 原因の分析
私の問題のGASのWebアプリでは、location.reload()によるページ再読み込み時に以下の処理が発生しました。
- HTMLの再パース(6,000行分)
- JavaScriptの再解釈
- GASテンプレートエンジンによるデータ注入(Base64データのデコード等)
- DOMの再構築
これらの処理が完了する前にブラウザが描画を試みると、画面が真っ白になるという現象が発生します。
特にGASでは、
doGet()
でテンプレートにデータを渡す際の初期化プロセスに時間がかかるため、通常のWebアプリケーションよりも問題が顕著に現れます。
✅ 解決策: Promise/async-awaitでデータ再取得
location.reload()を使わず、必要なデータのみをサーバーから再取得して画面を更新する方法に変更しました。
アプローチ
- google.script.runをPromiseでラップする
- async/awaitで可読性の高いコードにする
- サーバーから最新データを取得して、必要な部分のみを再描画
📝 実装方法
Step 1: Promiseヘルパー関数の作成
まず、google.script.runをPromiseでラップする汎用ヘルパー関数を作成します。
/**
* google.script.runをPromiseでラップするヘルパー関数
* @param {string} functionName - 呼び出すサーバー関数名
* @param {...any} args - サーバー関数に渡す引数
* @returns {Promise} - サーバーからの結果を返すPromise
*/
function callServerAsync(functionName, ...args) {
return new Promise((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
[functionName](...args);
});
}
Step 2: データ再取得関数の作成
サーバーから最新データを取得して画面を更新する関数を作成します。
/**
* サーバーから最新データを取得して画面を更新
* location.reload()の代わりに使用
*/
async function refreshPageData() {
try {
setLoading(true);
// サーバーから最新データを取得
const newData = await callServerAsync('getLatestData');
// グローバル変数を更新
window.appData = newData;
// 画面を再描画
renderPage(newData);
setLoading(false);
} catch (error) {
setLoading(false);
console.error('データ取得エラー:', error);
showErrorMessage('データの更新に失敗しました');
throw error;
}
}
Step 3: 既存コードの書き換え
従来のgoogle.script.runのコールバック形式から、async/awaitに書き換えます。
❌ 変更前
function executeProcess() {
if (!confirm('処理を実行しますか?')) return;
setLoading(true);
showMessage('処理中...', 'loading');
google.script.run
.withSuccessHandler(msg => {
setLoading(false);
showMessage(msg, 'success');
setTimeout(() => {
location.reload(); // ← 問題の箇所
}, 2000);
})
.withFailureHandler(error => {
setLoading(false);
showMessage('エラー: ' + error.message, 'error');
})
.processData();
}
✅ 変更後
async function executeProcess() {
if (!confirm('処理を実行しますか?')) return;
try {
setLoading(true);
showMessage('処理中...', 'loading');
// サーバー関数を呼び出し(async/await)
const msg = await callServerAsync('processData');
// 成功メッセージを表示
showMessage(msg + ' 完了しました!', 'success', 5000);
// 画面データを更新(リロードなし)
await refreshPageData();
} catch (error) {
setLoading(false);
showMessage('エラー: ' + error.message, 'error');
}
}
サーバー側(GAS)のコード例
/**
* クライアントに返すデータを取得
*/
function getLatestData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
const data = sheet.getDataRange().getValues();
// データを加工して返す
return {
records: data.slice(1), // ヘッダーを除く
lastUpdated: new Date().toISOString(),
summary: calculateSummary(data)
};
}
/**
* データ処理を実行
*/
function processData() {
// 何らかの処理を実行
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');
// ... 処理内容 ...
return '処理が正常に完了しました';
}
🎯 メリット
この解決策により、以下のメリットが得られました:
1. 信頼性の向上
✅ location.reload()による不確実な初期化を排除
✅ GASの再初期化エラーが発生しない
✅ 画面が真っ白になる問題を完全に解決
2. パフォーマンス向上
✅ ページ全体の再読み込みが不要
✅ 必要なデータのみをサーバーから取得(高速)
✅ 大きなHTMLファイルの再パースが不要
3. ユーザー体験の改善
✅ 画面のちらつきがなくなる
✅ ローディング表示がスムーズ
✅ 処理完了メッセージをゆっくり確認できる
4. コードの可読性とメンテナンス性
✅ async/awaitで非同期処理の流れが明確
✅ コールバック地獄を回避
✅ エラーハンドリングがtry-catchで統一
✅ callServerAsyncヘルパーは他の機能でも再利用可能
📊 比較表
| 項目 | location.reload() | データ再取得 |
|---|---|---|
| 画面の安定性 | ❌ 真っ白になることがある | ✅ 安定 |
| 処理速度 | ❌ 遅い(全体再読み込み) | ✅ 高速(差分のみ) |
| ネットワーク負荷 | ❌ 大きい | ✅ 小さい |
| コードの可読性 | △ コールバック形式 | ✅ async/await |
| カスタマイズ性 | ❌ 低い | ✅ 高い(更新箇所を選択可能) |
💭 注意点
タイミングの考慮
データ再取得のタイミングは、ユーザーにメッセージを見せる時間も考慮して設計すると良いでしょう。
// 成功メッセージを表示
showMessage(msg + ' 完了しました!', 'success', 5000);
// メッセージ表示中に裏でデータ更新
await refreshPageData();
グローバル状態の管理
refreshPageData()では、ページ初期化時と同じ状態変数の更新が必要です。DOMContentLoadedのロジックを関数化して再利用するのがオススメです。
// 初期化ロジックを関数化
function initializeAppState(data) {
window.appData = data;
window.currentView = 'dashboard';
// ... その他の状態変数
}
// DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
const initialData = decodeAndParseData();
initializeAppState(initialData);
renderPage(initialData);
});
// データ再取得時も同じロジックを使用
async function refreshPageData() {
const newData = await callServerAsync('getLatestData');
initializeAppState(newData); // 同じ関数を再利用
renderPage(newData);
}
📝 まとめ
GASのWebアプリでlocation.reload()が原因で画面が真っ白になる問題は、以下の方法で解決できました:
- google.script.runをPromiseでラップする汎用ヘルパーを作成
- async/awaitで非同期処理を簡潔に記述
- location.reload()を廃止し、サーバーから最新データのみを再取得
- 必要な部分のみを再描画することで高速化
この方法により、信頼性・パフォーマンス・コードの可読性すべてが向上しました。
GASで大規模なWebアプリを開発している方は、ぜひ試してみてください!