Xserver の共用レンタルで動く WordPress に、チャット機能を入れようとしたことはありますか。もし入れるなら、最初に WebSocket だけへ全部を賭けないほうがいい、というのが私の結論です。2025年秋、私は3方式を順に試して、WebSocket は接続できず、SSE は配信が安定せず、最後に Long Polling で素直に動きました。以下、なぜそうなったかを、起きた順に書きます。コードは考え方を確認する最小サンプルで、コピペでの本番運用を意図したものではありません。VPS や自前で Node.js を立てられる環境なら、答えは変わります。
検証環境:WordPress 6.8.3 / Xserver 共用レンタル / PHP 8.3.21。検証は2025年秋、記事更新は2026年5月5日。

WebSocket は、なぜ共用レンタルで止まったのか
チャットと聞いて最初に思い浮かんだのが WebSocket でした。接続を開きっぱなしにして双方向にやり取りする仕組みで、ローカルでは Node.js を立ててすんなり動きました。止まったのは本番です。WordPress は普通に表示できるのに、WebSocket の接続だけが成立しない。普通の fetch は通るのに、です。
原因は、最初の握手にありました。WebSocket は、いきなり WebSocket で始まるのではなく、まず普通の HTTP でつないで、Upgrade: websocket というヘッダーを載せて切り替えを申し込みます。サーバーが応じて初めて、双方向通信になる。VPS のようにリバースプロキシや WAF を自分で触れる環境なら、この Upgrade を転送する設定を書けばいいだけです。共用レンタルは違います。利用者が触れないプロキシや WAF が中間にいくつも挟まっていて、Upgrade を含むリクエストが、そのどれかで止まる。私の環境では、この握手が通りませんでした。2、3日いろいろ試して、共用レンタルで正面突破するのは無理だと見切りをつけました。WebSocket が悪いわけではありません。VPS や Cloudflare、Fly.io を前提にできるなら、いまでも第一候補です。
SSE は通った。なぜタイミングがそろわなかったのか
次に試したのが SSE です。サーバーからブラウザへ一方向にイベントを流し続ける仕組みで、HTTP に乗っているぶん、共用レンタルでも通りやすいと期待しました。実際、接続はできました。EventSource の通信が開いたまま、データも届く。WebSocket のように最初から失敗はしません。問題は、配信のタイミングでした。期待した間隔で届くこともあれば、しばらく無音のあと何件かまとめて来ることもある。同じコードを開き直しても、毎回そろうわけではない。
SSE は接続を開いたまま、サーバー側から少しずつ書き出します。ところが間に立つプロキシやバッファが、その少しずつをいったん溜めて、まとまってから流すことがあります。Nginx 配下なら X-Accel-Buffering: no で即時配信させる手がありますが、共用レンタルではサーバー全体の設定を変えられません。PHP 側で output_buffering を切ったり flush() を細かく入れたりしましたが、安定しませんでした。ニュース配信や進捗通知のように、多少前後しても困らない用途なら実用範囲かもしれません。チャットでは、送ったのに数十秒後にまとめて表示される、が致命的でした。
残った Long Polling は、なぜ動いたのか
残った選択肢が Long Polling でした。地味な仕組みですが、共用レンタルではこれがいちばん相性がよかった。動きはこうです。ブラウザがサーバーに新着はあるかと聞く。あればその場で返し、なければ一定時間待って、それでもなければ空で返す。受け取ったブラウザは、また次の問い合わせを始める。

大事なのは、1回1回が完結した普通の HTTP リクエストだということです。WebSocket のような Upgrade はなく、SSE のように接続を開いたまま流し続ける必要もない。だから間にプロキシやキャッシュや WAF が挟まっていても、普通のページアクセスと同じ扱いで通っていく。実際に試したら、Xserver の共用レンタルでも素直に動きました。サーバー側(PHP)は、since より新しいメッセージがあるかを見て、なければ少し待ってもう一度見る、を制限時間まで繰り返します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?php header("Content-Type: application/json; charset=utf-8"); header("Cache-Control: no-store"); $since = isset($_GET["since"]) ? (int) $_GET["since"] : 0; $deadline = microtime(true) + 18.0; // get_new_messages() は実運用では id > $since の DB 検索に置き換える $messages = []; $last_id = $since; while (microtime(true) < $deadline) { $messages = get_new_messages($since); if (!empty($messages)) { $last_id = end($messages)["id"]; break; } usleep(250000); } echo json_encode( ["messages" => $messages, "last_id" => $last_id], JSON_UNESCAPED_UNICODE ); |
ブラウザ側は、レスポンスが返るたびに次を投げるループです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | let lastId = 0; let backoff = 300; async function poll() { while (true) { if (document.hidden) { await sleep(1500); continue; } try { const res = await fetch(`/poll.php?since=${lastId}`, { cache: "no-store" }); const data = await res.json(); if (Array.isArray(data.messages) && data.messages.length) { for (const m of data.messages) { console.log("recv:", m); } lastId = data.last_id; } else { await sleep(60); } backoff = 300; } catch (e) { await sleep(backoff); backoff = Math.min(8000, Math.floor(backoff * 1.6)); } } } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } poll(); |
これだけでも、共用レンタルで安定して動きました。WebSocket のような滑らかさはありませんが、数秒以内に届くので、待たされる感覚はあまりありません。
ただ投げ続けるだけでは、なぜまずいのか
短い間隔で投げ続けると、サーバーへの負担がじわじわ増えます。WordPress は1リクエストで読み込むものが多いので、待ち方は最初から考えておきたい。私が意識したのは、この4つです。
- サーバー側の待ち時間を伸ばしすぎない。max_execution_time が30秒なら、待ちは15から20秒にして余裕を持たせる。ギリギリだと、たまにエラーになるユーザーが出て切り分けが面倒になります。
- 新着がないとき、すぐ次を投げない。空レスポンスのあとに短いスリープ(コードの await sleep(60))を入れるだけで、リクエスト数の見え方が変わります。
- タブが見えていないときは間引く。document.hidden を見て頻度を落とす。複数タブで開きっぱなしにする使い方で地味に効きます。
- エラーが出たら、リトライ間隔を広げる。失敗が続くほど待ちを伸ばし、上限で頭打ちにする指数バックオフ。障害時に勝手に暴走しない安心感が違います。
3方式を、自分の環境で起きたことで並べる
| 方式 | 仕組み | 私の環境での結果 |
|---|---|---|
| WebSocket | 双方向の常時接続(Upgrade で切り替え) | 接続そのものが成立しなかった |
| SSE | サーバーからの一方向ストリーミング | 接続はできたが、配信タイミングが安定しない |
| Long Polling | HTTP リクエストとレスポンスの繰り返し | 共用レンタル上で安定して動いた |
この表だけ見ると Long Polling が一番よく見えますが、そういう話ではありません。VPS や専用サーバー、Cloudflare Workers や Fly.io なら、WebSocket を選ぶ場面は多いはずです。あくまで Xserver の共用レンタル上の WordPress で、という前提での結果です。
即時性は、別のやり方でも作れる
Long Polling で気になるのは、WebSocket ほどの即時性が出ないことです。ただ、体験として、すべてを完全にリアルタイムにする必要はありません。自分が送ったメッセージは、サーバーの返答を待たずに先に画面へ並べてしまう。送信ボタンを押した瞬間に、自分のメッセージはもう表示されている。失敗が返ったときだけ、あとからエラー表示に差し替える。これを楽観的 UI 更新(Optimistic UI Update)と呼びます。相手から届くメッセージは Long Polling で数秒以内に取りに行く。業務システムでミリ秒の即時性が要件、というのでなければ、この体感で困る場面は意外と少ないです。
WordPress 7.0 のコアも、同じ場所に着地していた
この記事を書き直しているとき、WordPress 7.0 のリアルタイム共同編集の方針が公開されていました。Dev Note を読むと、同期方式のデフォルトが HTTP ポーリングになっていました。検討段階では WebRTC も候補だったものの、共用レンタルやネットワーク制限のある環境で動かないことを理由に却下した、と書かれています。WebSocket 対応のホスティングではフィルターで切り替えられるけれど、デフォルトはポーリング、という構成です。細かい背景は WordPress 7.0「Armstrong」リリースまとめ に書きました。共用レンタルで動くことを最優先したら、コア側もほぼ同じ場所に着地していた、というのは、少し心強い事実でした。
あなたなら、どの順で試しますか
同じ環境でチャットを入れるなら、最初から WebSocket だけに絞らず、Long Polling も視野に入れておくと切り分けが楽です。新しい方式から試して降りるのでも、枯れた方式から試して上がるのでも構いません。大事なのは、自分のサーバーで何が動いて何が動かないかを、早めに把握することです。
最後に、次にやる自分へのメモを兼ねて。検証中のエラーや挙動は、スクリーンショットでも走り書きでも、あとで読める形で残しておいたほうがいいです。私は今回それを残し損ねて、この記事も記憶に頼れる範囲までしか書けませんでした。当時の DevTools の画面があれば、もっと具体的に書けたはずなのに、と思います。あなたがこれから同じ環境にぶつかるなら、その一手間が、未来のあなたを助けてくれます。あなたなら、WebSocket と Long Polling、どちらから試しますか。
参考にした資料
- MDN Web Docs: Protocol upgrade mechanism(確認日: 2026年5月5日)
- MDN Web Docs: WebSocket API(確認日: 2026年5月5日)
- MDN Web Docs: Server-sent events(確認日: 2026年5月5日)



コメント