はじめに
チャットアプリを作るとき、ユーザーが新しいメッセージを受け取るためには、画面を手動で更新するのではなく、自動でサーバーへ問い合わせる仕組みが必要になります。
この仕組みを一般に「ポーリング」と呼びます。簡単に言えば、一定時間ごとにサーバーに「新しいメッセージがあるか?」と聞きに行く処理です。
ポーリングは、チャットアプリだけでなく、通知の確認、状態の更新、ライブ情報の取得など幅広い場面で使われます。
まず「なぜポーリングが必要なのか」を理解し、そのあとでJavaScriptの基本構成を見ていきます。
ポーリングとは何か
ポーリングとは、一定時間ごとにサーバーへリクエストを送って、最新情報を取得する仕組みです。
たとえばチャットでは、次のような流れになります。
1. 画面を開く
2. 5秒ごとにサーバーへ問い合わせる
3. 新しいメッセージがあれば画面に追加する
4. 画面更新後も同じ処理を継続する
この考え方があると、ユーザーがページをリロードしなくても、新しい会話が自動で反映されます。
ポーリングのメリットは、実装がシンプルでわかりやすいことです。特に小規模なWebアプリでは、リアルタイム通信よりも導入しやすいです。
なお、この記事では WebSocket などの双方向通信を前提にせず、単純な HTTP ベースのポーリングを使う設計を前提にしています。これは、簡易チャットや社内向けの小さなWebアプリであれば、環境やサーバー構成を問わず設置しやすくなるためです。
WebSocket はリアルタイム性が高い一方で、サーバー実装や接続管理、ネットワーク制御が必要になるため、シンプルな導入を重視する場合にはハードルが上がります。逆に、ポーリングはfetch だけで済み、PHPやNode.js、静的ホスティングなど、環境を問わずに導入しやすいのが大きな利点です。
なぜチャットではポーリングが必要なのか
チャットは、相手がメッセージを送った後に、相手側の画面だけではなく自分の画面にもすぐ反映される必要があります。
一度だけデータを取得して終わりではなく、定期的に確認して新しいデータがあるかを見に行く必要があります。
そのため、ポーリングはチャットの基礎として非常に重要です。
この処理の基本は、次のようなイメージです。
ユーザーが画面を開く
-> 最初に 一回だけ取得
-> その後、一定間隔で再取得
-> 新着メッセージがあれば追加表示
この流れを理解しておくと、チャットアプリの基本設計がしやすくなります。
WebSocket を使わない前提で考える
この手法は、リアルタイム性の高いチャットを本格実装するアーキテクチャではなく、環境を問わず設置可能な簡易チャットを前提に考えます。
たとえば、以下のようなケースを想定できます。
- 小規模な営業支援チャット
- 社内向けの簡易問い合わせ窓口
- 既存のWebサイトに埋め込む簡易チャット
- PHP や静的サイトの環境に簡単に導入したい場合
こうした環境では、WebSocket を導入するよりも、一定間隔で API を叩いて最新メッセージを取得するポーリングのほうが、設計が簡潔で運用しやすいです。
もちろん、WebSocket には即時性やサーバー負荷の面で優位性がありますが、この記事の目的は「最小構成で簡単に動くチャット」を作ることです。環境の制約がある場合に、ポーリングは非常に現実的な選択肢になります。
setInterval を使って定期実行する
JavaScriptでは、定期的に処理を実行したいときに setInterval() を使います。
setInterval(() => {
fetchMessages();
}, 5000);
この例では、5秒ごとに fetchMessages() が実行されます。
fetchMessages() の中では、通常はサーバーへ API を呼び出して最新メッセージを取得します。
function fetchMessages() {
fetch('/api/messages')
.then((response) => response.json())
.then((data) => {
console.log('新しいメッセージを取得しました', data);
})
.catch((error) => {
console.error('取得に失敗しました', error);
});
}
この形が、ポーリングの最もシンプルな実装例です。
取得したメッセージを画面に追加する
サーバーからメッセージ一覧を取得できたら、画面に追加表示します。
const chatWindow = document.getElementById('chatWindow');
function renderMessage(message) {
const item = document.createElement('div');
item.className = 'message';
item.textContent = message.text;
chatWindow.appendChild(item);
}
function fetchMessages() {
fetch('/api/messages')
.then((response) => response.json())
.then((messages) => {
messages.forEach((message) => {
renderMessage(message);
});
});
}
ここで大事なのは、取得したすべてのメッセージを毎回追加してしまうと重複してしまうことです。
そのため、通常は「最後に取得した ID」や「最終更新時間」を管理して、既に表示済みのメッセージを重複表示しないようにします。
重複を防ぐ設計が重要
ポーリングでは、メッセージが毎回同じ内容で追加されてしまうと、画面が重複表示されます。
この問題を防ぐために、次のような管理が必要です。
- 最後に読み込んだメッセージ ID
- 最後に取得した時刻
- 既に表示済みのメッセージの一覧
例えば、サーバーが id を返すなら、次のように管理できます。
let lastMessageId = 0;
function fetchMessages() {
fetch('/api/messages?after=' + lastMessageId)
.then((response) => response.json())
.then((messages) => {
if (!messages.length) return;
messages.forEach((message) => {
renderMessage(message);
});
lastMessageId = messages[messages.length - 1].id;
});
}
ここでは、after= という条件で「指定IDより新しいメッセージだけ」を取得しています。
こうすることで、すでに表示済みの内容を何度も足すことを防げます。
取得間隔を考える
ポーリングは、間隔が短すぎるとサーバーに負荷がかかりますし、長すぎると新着メッセージの反映が遅くなります。
一般的には、2〜5秒程度がよく使われます。
setInterval(() => {
fetchMessages();
}, 3000);
この間隔は、アプリの性質に応じて調整します。
- 重要な通知: 短い間隔
- 低負荷が必要: 少し長めの間隔
- サーバー負荷を減らしたい: まとめて取得
このように、ポーリングは「定期実行」だけでなく、「頻度の設計」も重要です。
ポーリングを止める方法
アプリが閉じられたり、画面が不要になったりしたときには、ポーリングを止める必要があります。
setInterval() は、返り値を保持しておくことで clearInterval() で止められます。
const timer = setInterval(() => {
fetchMessages();
}, 3000);
// 必要になったら停止
clearInterval(timer);
この実装は、ページ遷移や終了時の後処理で使いやすいです。
チャット画面を閉じるときや画面遷移時に停止しておくと、バックグラウンドで無駄なアクセスをしなくなります。
実際の設計で意識したいこと
ポーリングは、見た目だけを考えるよりも、次の点が重要です。
1. 重複取得を避ける
前回取得済みのメッセージがまた追加されないようにする。
2. サーバー負荷を考える
短すぎる間隔は負荷につながる。できるだけ適切な時間間隔を設定する。
3. 取得失敗時も考える
通信失敗時に例外処理を入れて、エラーが続いてもアプリが止まらないようにする。
4. 画面表示とデータ取得を分ける
データ取得の処理と表示処理を別関数に分けると、管理しやすくなります。
例: 最小構成のポーリング実装
let lastMessageId = 0;
let pollTimer = null;
function renderMessage(message) {
const el = document.createElement('div');
el.className = 'message';
el.textContent = message.text;
document.getElementById('chatWindow').appendChild(el);
}
function fetchMessages() {
fetch('/api/messages?after=' + lastMessageId)
.then((response) => response.json())
.then((messages) => {
if (!messages.length) return;
messages.forEach((message) => {
renderMessage(message);
});
lastMessageId = messages[messages.length - 1].id;
})
.catch((error) => {
console.error('ポーリングに失敗しました:', error);
});
}
pollTimer = setInterval(fetchMessages, 3000);
この構造だけでも、基本的なチャットの自動更新は作れます。
ズームアップして考えると、以下の役割が分かれています。
-
fetchMessages(): データ取得 -
renderMessage(): 画面への反映 -
setInterval(): 一定間隔で取得 -
lastMessageId: 重複防止
この分担が、実装を整理しやすくします。
まとめ
チャットのポーリング機能は、ただ定期的に通信をするだけではなく、次の考え方が重要です。
- 何を定期的に取得するのか
- どのメッセージが新しいのか
- 画面に重複して追加しないか
- 定期実行を止めるタイミングはいつか
この考え方を理解しておくと、チャットアプリの基礎が自然に身に付きます。
ポーリングはシンプルですが、実際にはデータ管理の設計が結構重要です。
次の一歩としては、setInterval() を使った最小構成を作り、そこで必要になる「最後に取得したID」の管理を実装してみるのが最も理解しやすいです。
この考え方は、チャットだけでなく通知アプリや新着情報の表示など、Webアプリの基本となる考え方として広く使えます。
- チャポット: https://chapot.net/
