2026年2月14日土曜日

スマホ版の Chrome ではちゃんと DOM を定義してから値を設定したりするほうがいい

スマホ版の Chrome ではちゃんと DOM を定義してから値を設定したりするほうがいい

概要

動的に appendChild とかするとリソースを消費しすぎと判断して描画を強制的に停止するようです

環境

  • iOS 26.2.1
  • Chrome 144.07559.95

ダメなコード

function addMessage(msg) {
  // avoid duplicates; update indicator if already present
  const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
  if (existing) {
    const ind = existing.querySelector(".read-indicator");
    if (ind) {
      updateIndicator(ind, existing, msg.reads || []);
    }
    return;
  }
  // 新着メッセージ(相手からのもの)に対する通知・未読処理
  if (msg.message && msg.client_id !== CLIENT_ID) {
    // ブラウザ通知の呼び出し
    showBrowserNotification(msg);
    // タブが裏側にある場合、未読カウントを増やす
    if (document.visibilityState !== "visible") {
      unreadCount++;
      updateTabTitle();
    }
  }
  // new message
  const li = document.createElement("li");
  li.className = msg.client_id === CLIENT_ID ? "me" : "other";
  li.dataset.msgId = msg.id;

  const textSpan = document.createElement("span");
  textSpan.className = "msg-text";
  textSpan.textContent = msg.message;

  const ind = document.createElement("span");
  ind.className = "read-indicator";

  if (msg.client_id === CLIENT_ID) {
    // 自分のメッセージ: サーバーの reads 情報を使って表示
    updateIndicator(ind, li, msg.reads || []);
    li.appendChild(textSpan);
    li.appendChild(ind);
  } else {
    // 相手のメッセージ
    updateIndicator(ind, li, msg.reads || []);

    li.addEventListener("click", () => {
      if (isRead(msg.id)) return;
      setRead(msg.id);
      ind.classList.remove("unread");
      ind.classList.add("read");
      ind.title = "既読";
      // notify server
      markRead(msg.id);
    });

    li.appendChild(textSpan);
    li.appendChild(ind);
  }

  document.getElementById("log").appendChild(li);
}

良いコード

function addMessage(msg) {
  // 1. 基本チェック
  if (!msg || !msg.id) return;
  // read_event は applyReadEvent で処理されるため、ここではメッセージ本体がないものを除外
  if (msg.type === "read_event" || !msg.message) return;

  // 2. 重複チェック
  const log = document.getElementById("log");
  if (!log) return;
  const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
  if (existing) return;

  // 3. 要素の作成
  const li = document.createElement("li");

  // 安全な ID チェック
  const currentId =
    typeof CLIENT_ID !== "undefined" ? CLIENT_ID : getClientId();

  li.className = msg.client_id === currentId ? "me" : "other";
  li.setAttribute("data-msg-id", msg.id); // dataset ではなく setAttribute を使用(古いブラウザ対策)

  const textSpan = document.createElement("span");
  textSpan.className = "msg-text";
  textSpan.textContent = String(msg.message); // 明示的に文字列変換

  const ind = document.createElement("span");
  ind.className = "read-indicator unread"; // デフォルトは unread

  // 4. 要素の組み立て(先に DOM に追加してから細部を調整するのがスマホでは安定します)
  li.appendChild(textSpan);
  li.appendChild(ind);
  log.appendChild(li);

  // 5. 既読インジケーターの更新(エラーが起きても表示自体は維持する)
  try {
    updateIndicator(ind, li, msg.reads || []);
  } catch (e) {
    console.warn("Indicator error ignored for safety", e);
  }

  // 6. イベントリスナー(相手のメッセージのみ)
  if (msg.client_id !== currentId) {
    li.addEventListener("click", function () {
      try {
        if (isRead(msg.id)) return;
        setRead(msg.id);
        ind.classList.remove("unread");
        ind.classList.add("read");
        markRead(msg.id);
      } catch (err) {
        console.error(err);
      }
    });

    // 通知処理(エラー回避のため try-catch)
    try {
      showBrowserNotification(msg);
    } catch (e) {}
  }

  // 7. スクロール
  log.scrollTop = log.scrollHeight;
}

比較

堅牢性とエラーハンドリング

「良いコード」は、外部要因でプログラムが止まらないよう工夫されています。

項目 ダメなコード 良いコード
ガード句 データの存在チェックが甘く、msg.idが欠落しているとエラーになる可能性がある。 冒頭で `!msg
例外処理 updateIndicator 等でエラーが起きると、後続の処理(DOM追加等)が止まる。 try-catch を活用し、一部の表示エラーが起きてもアプリ全体が止まらない。
変数の安全性 CLIENT_ID がグローバルにある前提。 typeof チェックやフォールバック(getClientId())を用意している。

責務の明確化と可読性

コードが整理されているため、後からの修正が容易です。

  • 単一責任の原則:
    • ダメ: addMessage 内で通知、未読カウント、DOM操作、重複チェックが混ざり、条件分岐(if (msg.client_id === CLIENT_ID) … else …)で同じような appendChild が重複している。
    • 良い: メッセージの型判定(read_event かどうか)を fetchMessages 側で仕分け、addMessage は「表示」に専念している。
  • DRY (Don’t Repeat Yourself):
    • ダメ: 自分のメッセージと相手のメッセージで、インジケーターの追加処理を2回書いている。
    • 良い: 共通の組み立て(li への追加)を先に行い、差分(イベントリスナー)だけを条件分岐で書いている。

ユーザー体験 (UX) と実用性

ブラウザやネットワークの特性を考慮した実装になっています。

  • スクロール処理: 「良いコード」では追加後に log.scrollTop = log.scrollHeight を行い、常に最新メッセージが見えるように配慮されています。
  • 通信の最適化: fetchMessages でキャッシュ回避のためのタイムスタンプを追加しており、古いデータを掴まされるリスクを減らしています。
  • 暗黙の型変換への対処: String(msg.message) とすることで、数値や null が送られてきても textContent で安全に表示できます。

改善のポイント(まとめ)

  • ❌ ダメな点
    • 重複コード: li.appendChild(textSpan) などが if/else 両方にあり、修正漏れの原因になる。
    • 不親切なエラー: ネットワークエラーやデータ不備で JS が停止し、画面が真っ白になるリスクがある。
    • 状態管理の欠如: スクロール位置の調整がないため、ユーザーが手動でスクロールする必要がある。
  • ✅ 良い点
    • 徹底したバリデーション: 「データはあるか?」「型は正しいか?」を常に疑っている。
    • 保守性の高い構造: コメントで工程(1〜7)が区切られており、どこで何をしているか一目でわかる。
    • API設計への配慮: lastId の更新により、重複してメッセージを取得しない仕組みが整っている。

最後に

スマホ版の Chrome も意識して JavaScript を書かないとダメなようです

0 件のコメント:

コメントを投稿