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

fetch や res.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をコピーしました