JavaScript | 非同期処理:実務での非同期制御 - ローディング表示

JavaScript JavaScript
スポンサーリンク

ローディング表示を一言でいうと

ローディング表示は、
「今、アプリはちゃんと動いているよ。止まっているんじゃなくて“待っているだけ”だよ」
とユーザーに伝えるためのサインです。

非同期処理(fetch / API 通信)は、
「ボタンを押してから結果が返るまでに“間”がある」のが当たり前です。
その“間”を何も表示しないと、ユーザーはこう感じます。

「押せてない?」「フリーズした?」「もう一回押した方がいい?」

ローディング表示は、
この不安を消すための“心のインターフェース”でもあります。


非同期処理とローディング表示の基本的な関係

「処理中」と「処理完了」の境目をコードで持つ

ローディング表示をちゃんと扱うには、
まず「今は処理中かどうか」をコード上で表現する必要があります。

一番シンプルなのは、
「処理開始時に true、終わったら false にするフラグ」です。

let isLoading = false;

async function loadUsers() {
  isLoading = true;   // 処理開始
  try {
    const res = await fetch("/api/users");
    const data = await res.json();
    renderUsers(data);
  } finally {
    isLoading = false; // 成功でも失敗でも必ず処理終了
  }
}
JavaScript

この isLoading を、
画面の表示と結びつけます。

function renderLoading() {
  const el = document.getElementById("loading");
  el.style.display = isLoading ? "block" : "none";
}
JavaScript

そして、状態が変わるたびに renderLoading() を呼ぶイメージです。

ここが重要です。
ローディング表示は「勝手に出たり消えたりするもの」ではなく、
“処理中かどうか”という状態をコードで管理し、それを UI に反映する設計
だと捉えると、
一気に筋が通ります。

finally で「必ず消す」を保証する

非同期処理では、
エラーが起きてもローディング表示が消えない、という事故がよく起きます。

例えば、こういうコードは危険です。

async function loadUsers() {
  showLoading();
  const res = await fetch("/api/users");
  const data = await res.json();
  hideLoading(); // ここに来る前にエラーが起きたら、ローディングが消えない
}
JavaScript

fetchres.json() でエラーが起きたら、
hideLoading() まで到達しません。

これを防ぐために使うのが try ... finally です。

async function loadUsers() {
  showLoading();
  try {
    const res = await fetch("/api/users");
    const data = await res.json();
    renderUsers(data);
  } catch (err) {
    console.error(err);
    showErrorMessage("ユーザーの取得に失敗しました");
  } finally {
    hideLoading(); // 成功でも失敗でも必ず実行される
  }
}
JavaScript

ここがとても重要です。
「ローディングを出す」と「ローディングを消す」は、
必ずセットで書く。
そして“どんな結果でも必ず消す”ために finally を使う。

このパターンを体に染み込ませておくと、
「ぐるぐるが永遠に回り続けるバグ」をかなり防げます。


実務でよくあるローディング表示のパターン

パターン1:画面全体を覆うローディング(フルスクリーン)

ページ全体のデータを読み込むときなどに使うパターンです。

HTML 例:

<div id="page-loading" class="loading-backdrop" style="display: none;">
  <div class="spinner">Loading...</div>
</div>

JavaScript 例:

function showPageLoading() {
  document.getElementById("page-loading").style.display = "flex";
}

function hidePageLoading() {
  document.getElementById("page-loading").style.display = "none";
}

async function initPage() {
  showPageLoading();
  try {
    const res = await fetch("/api/page-data");
    const data = await res.json();
    renderPage(data);
  } catch (err) {
    console.error(err);
    showErrorMessage("ページの読み込みに失敗しました");
  } finally {
    hidePageLoading();
  }
}
JavaScript

これは、
「このページは今“読み込み中”で、まだ操作できません」
という状態をはっきり示したいときに使います。

パターン2:ボタン単位のローディング(スピナー+無効化)

フォーム送信やボタン押下時によく使うパターンです。

HTML 例:

<button id="save-button">
  <span class="label">保存</span>
  <span class="spinner" style="display: none;">...</span>
</button>

JavaScript 例:

const saveButton = document.getElementById("save-button");

function setSaveButtonLoading(isLoading) {
  saveButton.disabled = isLoading;
  saveButton.querySelector(".label").style.display = isLoading ? "none" : "inline";
  saveButton.querySelector(".spinner").style.display = isLoading ? "inline" : "none";
}

async function onClickSave() {
  setSaveButtonLoading(true);
  try {
    await saveForm();
    showSuccessMessage("保存しました");
  } catch (err) {
    console.error(err);
    showErrorMessage("保存に失敗しました");
  } finally {
    setSaveButtonLoading(false);
  }
}

saveButton.addEventListener("click", onClickSave);
JavaScript

ここでのポイントは、

ボタンを無効化して「連打」を防ぐ
ラベルを隠してスピナーを出すことで「今このボタンが処理中」と分かる

という 2 点です。

ここが重要です。
「ローディング表示」と「操作の無効化」はセットで考えると、
ユーザーの誤操作や二重送信をかなり防げます。

パターン3:部分的なローディング(リストだけ、カードだけ)

ページ全体ではなく、
「このリストだけ読み込み中」「このカードだけ更新中」
という局所的なローディングもよくあります。

例えば、ユーザー一覧だけにローディングを出す場合。

<div id="user-list">
  <div id="user-list-loading" style="display: none;">読み込み中...</div>
  <ul id="user-list-items"></ul>
</div>
function setUserListLoading(isLoading) {
  document.getElementById("user-list-loading").style.display = isLoading ? "block" : "none";
}

async function loadUsers() {
  setUserListLoading(true);
  try {
    const res = await fetch("/api/users");
    const data = await res.json();
    renderUserList(data);
  } catch (err) {
    console.error(err);
    showErrorMessage("ユーザー一覧の取得に失敗しました");
  } finally {
    setUserListLoading(false);
  }
}
JavaScript

こうすると、
ページ全体はそのまま操作できるけれど、
「ユーザー一覧のところだけ“今読み込み中”」という状態を表現できます。


「いつローディングを出すか」「どれくらい出すか」を考える

短すぎる処理にローディングを出しすぎない

実務では、
「何でもかんでもローディングを出せばいい」というわけではありません。

処理が 100ms くらいで終わるのに、
毎回スピナーがチカチカ出たり消えたりすると、
逆にうるさく感じます。

そこでよくやるのが、
「一定時間以上かかりそうならローディングを出す」という工夫です。

let loadingTimer = null;

function showLoadingWithDelay(delay = 200) {
  loadingTimer = setTimeout(() => {
    showLoading();
  }, delay);
}

function hideLoadingWithDelay() {
  clearTimeout(loadingTimer);
  hideLoading();
}

async function loadSomething() {
  showLoadingWithDelay(200); // 200ms 以上かかったらローディング表示
  try {
    const res = await fetch("/api/data");
    const data = await res.json();
    render(data);
  } finally {
    hideLoadingWithDelay();
  }
}
JavaScript

こうすると、
「一瞬で終わる処理にはローディングを出さない」
「少し時間がかかるときだけ出す」
というバランスが取れます。

ここが重要です。
ローディング表示は「多ければ多いほど良い」ではなく、
“ユーザーが不安になる時間帯” にだけ出すのが理想です。
そのために、遅延表示というテクニックがよく使われます。

ローディング中に「何が起きているか」を伝える

単に「ぐるぐる」だけ出すより、
「何をしているのか」を一言添えると、
ユーザーの安心感がかなり変わります。

例えば、

「ユーザー情報を読み込んでいます…」
「保存しています…」
「検索結果を取得しています…」

などです。

コード的には、
ローディング用の要素にメッセージを差し込むだけです。

function showLoading(message = "読み込み中です…") {
  const el = document.getElementById("loading");
  el.textContent = message;
  el.style.display = "block";
}
JavaScript

呼び出し側でこう使えます。

showLoading("ユーザー情報を読み込んでいます…");
JavaScript

複数の非同期処理が同時に走るときのローディング制御

「どれか一つでも処理中ならローディングを出したい」問題

実務では、
同じ画面で複数の API を同時に叩くことがよくあります。

例えば、

ユーザー情報を取得
通知一覧を取得
おすすめ情報を取得

など。

このとき、
単純にそれぞれで showLoading() / hideLoading() を呼ぶと、
先に終わった処理が hideLoading() を呼んでしまい、
まだ他の処理が動いているのにローディングが消えてしまう、
という問題が起きます。

これを防ぐために、
「今何件の処理が動いているか」をカウントする方法があります。

let loadingCount = 0;

function showGlobalLoading() {
  loadingCount++;
  document.getElementById("global-loading").style.display = "block";
}

function hideGlobalLoading() {
  loadingCount = Math.max(loadingCount - 1, 0);
  if (loadingCount === 0) {
    document.getElementById("global-loading").style.display = "none";
  }
}
JavaScript

使い方はこうです。

async function loadUser() {
  showGlobalLoading();
  try {
    const res = await fetch("/api/user");
    const data = await res.json();
    renderUser(data);
  } finally {
    hideGlobalLoading();
  }
}

async function loadNotifications() {
  showGlobalLoading();
  try {
    const res = await fetch("/api/notifications");
    const data = await res.json();
    renderNotifications(data);
  } finally {
    hideGlobalLoading();
  }
}
JavaScript

こうすると、

最初の処理開始 → loadingCount = 1 → ローディング表示
二つ目の処理開始 → loadingCount = 2 → ローディング継続
一つ目が終わる → loadingCount = 1 → まだ表示
二つ目が終わる → loadingCount = 0 → ローディング非表示

という流れになります。

ここが重要です。
複数の非同期処理が同時に走る場面では、
「ローディングの ON/OFF を単純なフラグではなく“カウント”で管理する」
という発想がとても役に立ちます。


初心者として「ローディング表示」で本当に押さえてほしいこと

ローディング表示は、
「処理が遅いから仕方なく出すもの」ではなく、
「ユーザーの不安を減らすためのコミュニケーション」 だと捉える。

コード上では、
「処理中かどうか」を状態として持ち、
その状態を UI に反映する、という設計にする。

showLoading()hideLoading() は必ずセットで書き、
どんな結果でも必ず消すために try ... finally を使う。

ボタン単位・部分単位・画面全体など、
「どの範囲をローディング状態にするか」を意識して選ぶ。

複数の非同期処理が同時に走る場合は、
「今何件処理中か」をカウントしてローディングを制御する、
というパターンを覚えておく。

そして何より、
「この待ち時間、ユーザーは何を感じているだろう?」
「何が見えていたら安心できるだろう?」

という視点でローディング表示を設計すると、
ただのスピナーが「気遣いのある UI」に変わっていきます。

今書いている非同期処理の中で、
「ここ、ユーザーから見ると“無言の待ち時間”になってない?」と感じる場所があったら、
そこに小さなローディング表示を一つ置いてみてください。
それだけで、アプリの“優しさ”が一段変わります。

タイトルとURLをコピーしました