概要
動的に 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 件のコメント:
コメントを投稿