バックエンドの PHP 案件といえば、Linux 上の Apache や PHP-FPM で動かす LAMP が当たり前だったので、はじめて「ブラウザだけで WordPress が動く」というのを聞いた時はどうもピンときませんでした。
調べていくうちに、その裏側には普段の LAMP とは違う仕組みがあることが分かり、さらにサーバー側 PHP を置かず、ブラウザ内の PHP と JavaScript を組み合わせたアプリも作れそうだ、と思うようになりました。そこで今回は、その仕組みを使って試作してみようと思い至った次第です。
きっかけになったのは、WordPress Playground を知ったことからなので、まずはそこから書きます。
1)ブラウザだけで WordPress? Playground を知った話
WordPress の案件で、プラグインを比較・選定するために情報を探していたところ、 WordPress Playground を知りました。触ってみると、ブラウザだけで WordPress の管理画面やサイトが立ち上がります。自分の中では「WordPress = サーバーを用意してから」というイメージだったので、PHP サーバーを立てずにすぐ動くこと自体が意外でした。加えて、プラグインやテーマの動作確認も、思ったよりそこそこ速いと感じました。
バックエンド担当の自分にとっては便利さもさることながら、「これ、裏側はどうなっているのだろう?」という点に興味を持ちました。
2)裏側を覗いたら WebAssembly.あ、PHP も動くの?
「どうやってブラウザだけで動いているのか」を調べると、キーワードとして出てきたのが WebAssembly(WASM)でした。
webassembly.org や MDN の説明を踏まえると、WebAssembly は次のようなものです。
- ブラウザなどで動く、低水準のバイナリ命令フォーマット(バイトコード)
- C / C++ / Rust などの言語のコンパイル先として設計されている
- コンパクトで、ネイティブに近い速度で実行することを目指している
- JavaScript は置き換えではなく、併用。JavaScript API 経由で WASM モジュールを読み込み、機能を共有できる
つまり「手で書くアセンブリ」というより、「他言語で書いた処理を、Web 上で速く動かすための実行形式」という理解が近いです。
WordPress Playground も、この仕組みを土台にしているそうです。公式ドキュメントやリポジトリでも、ブラウザ上で WebAssembly 版の PHP を使って WordPress を動かす、と説明されています。 (参考: About Playground , WebAssembly PHP , GitHub: wordpress-playground )
使われる場面の例としては、次のようなものがあります。
- 画像・動画・ゲームなど、重い処理をブラウザ側で行う
- C / C++ / Rust などで書いた既存の資産を Web に載せる
- ブラウザ上で、ある程度まとまったアプリを動かす
「ブラウザの中で、普段サーバー側でやっていることに近い処理を載せるための土台」というイメージでしょうか。実際、身近なサービスでも使われている例があり、 Google Earth(Web 版)や Amazon Prime Video、Disney+ での WebAssembly 利用例が公式・技術ブログなどで紹介されています。
ここまでが WebAssembly 一般の話です。
Playground が WebAssembly 版の PHP を使っていると分かったので、WordPress なしでも、その PHP ランタイムをブラウザで使えるのではないかと調べたところ、ブラウザ向けの PHP ランタイム(いわゆる php-wasm)があることが分かりました。
ちなみに私は、最初「PHP のソースが全部 WASM になるの?」と思ってしまっていましたが、実際には、次のように分かれます。
- WASM になるのは PHP エンジン本体(インタープリタ)
- 自分で書いた PHP のソースは WASM に変換されない。ブラウザ内の仮想ファイルシステム上で、いつもどおり解釈実行される
- JavaScript は WASM にはならない(ブラウザがそのまま実行)
つまり「アプリ全体が1つのネイティブ実行ファイルになる」というより、「ブラウザ上で動く Web アプリ一式の中に、PHP ランタイムが入っている」というイメージが近いです。ちなみにPHP ランタイムを使って、ブラウザ上で Laravel を動かす試みや、PHP のサンドボックスアプリ(ブラウザ内でコードを試せる環境)などの実例があるようです。
3)知るだけでもどうもピンとこないので、試しに作ってみる
仕組みを知っても、どうもピンとこないところが残っていたので、試しに作ってみることにしました。
もう一つの動機もあります。先日のブログでは、NativePHP for Mobile を使用したモバイルアプリでの位置情報取得を試しました。そのなかで、環境構築やネイティブ連携など、なかなか難しい部分にも当たりました。そこでもう少し、「位置情報+PHP」をモバイルネイティブより手軽に試作できないか、と考えた次第です。
試作する題材としては、位置情報を使って地図上に歩いたルートをトレースする機能がよさそうだと考えました。その結果、「お散歩の記録を残す」アプリなら、現在地の表示、移動の軌跡、距離の計算といった要素をひと通り試せるのではないか、と思い至りました。
このような動機と経緯から、今回は PHP を使用したブラウザだけで動くお散歩記録アプリを試作することにしました。サーバー側の PHP は使わず、位置情報や地図はブラウザ API、距離判定やログ生成はブラウザ内の PHP、という役割分担で、どこまで現実的に作れるかを確かめています。
4)できあがった「お散歩トラッカー」のご紹介
アプリ名は「お散歩トラッカー」です。できることは次のとおりです。
- 「お散歩開始」で現在地と周辺地図を表示
- 100m 移動するごとに軌跡とピンを追加
- 観光名所・史跡(いわゆる POI=興味のある地点)に近づくと写真撮影を案内
- 「お散歩終了」で移動距離を表示
- ログを JSON(詳細)と GPX(軌跡)で保存
地図には Leaflet、名所データには OpenStreetMap の Overpass API を使っています。位置情報の使用が許可されていない場合や、実際に歩かずに動作確認したいときは、メニューから「デモ」を選ぶと、東京駅〜皇居周辺を自動で歩く動きを再現できます。
起動直後の画面は次のようなイメージです。

お散歩開始後は、地図上に現在地が追従します。

100m ごとにピンが付き、軌跡が残ります。

名所に近づくと、写真撮影の案内が出ます。

終了すると距離が表示され、ログを保存できます。

保存した JSON を、別画面の「お散歩ログビューアー」で読み込むと地図上で見返すことができます。

5)誰が何を担当? ソースと外のサービスたち
全体の関係と、処理の分担は、次の構成図にまとめました。

ブラウザ上では、地図・位置情報・カメラ・外部 API への通信を JavaScript(Leaflet)が行い、距離計算や 100m 判定、名所への接近判定、ログ生成は php-wasm 上の PHP が行うように分担しています。
外部サービスとしては、公開には Cloudflare Pages を使い、地図タイルは OpenStreetMap(OSM)、名所データは Overpass API から取得しています。Overpass への問い合わせは、Cloudflare Pages Functions 経由のプロキシを置いています。
流れとしては「現在地を取る → 周辺の名所を取る → PHP で判定する → 地図に反映する」という順番です。
実際のソースツリーは、だいたい次のとおりです(主要なものだけ抜粋しています)。
public_html/
├── index.html # お散歩トラッカー本体
├── log-viewer.html # お散歩ログビューアー(保存した JSON を地図で見返す画面)
├── css/
│ └── style.css # スタイル
├── js/
│ ├── app.js # UI・GPS・写真・デモ制御
│ ├── php-bridge.js # php-wasm の起動と PHP 呼び出し
│ ├── map.js # Leaflet(地図・軌跡・ピン)
│ ├── overpass.js # Overpass API への問い合わせ
│ ├── demo-route.js # 東京駅〜皇居一周のデモ経路
│ └── log-viewer.js # ログビューアー側の表示処理
├── php/
│ ├── bootstrap.php # action の振り分け入口
│ ├── WalkService.php # 散歩セッションの本処理
│ ├── GeoCalculator.php # 距離計算
│ ├── OverpassClient.php # POI の正規化・接近判定
│ └── WalkLogger.php # JSON / GPX の生成
├── functions/
│ └── api/
│ └── overpass.js # Cloudflare Pages Functions(プロキシ)
├── vite.config.js # 開発用 HTTPS・プロキシ設定
├── package.json # 依存関係・deploy スクリプト
└── dist/ # ビルド成果物(Cloudflare Pages へデプロイ)
ローカル開発環境では Vite を使っています。Vite は、フロントエンド向けのツールで、開発用サーバーの起動や公開用のビルド(JS/CSS などのまとめ)を行います。
今回は、地図(Leaflet)や php-wasm が npm のパッケージとして配られており、しかもブラウザ上で JavaScript と WASM を組み合わせて動かす構成です。そのため、HTML を単に開くだけでは足りず、次のような理由から Vite を使用しています。
- npm パッケージの取り込みが必要
- .wasm を扱いやすい
- 公開用の静的ファイルを生成する必要がある
- 位置情報(Geolocation API)には HTTPS が必要で、ローカルでも HTTPS で起動できる
上記にもありますが、位置情報(Geolocation API)を使う都合で HTTPS が必要です。通常の http://localhost だとブラウザ側で位置が取れないため、Vite 公式の @vitejs/plugin-basic-ssl を入れ、vite.config.js で有効にしています。
vite.config.js(抜粋)
import basicSsl from '@vitejs/plugin-basic-ssl';
export default defineConfig({
plugins: [basicSsl()],
server: {
https: true,
// ...
},
});
これで npm run dev すると、ターミナルに https://localhost:5173/ の URL が出ます(自己署名証明書なので、初回はブラウザで警告が出ることがあります)。


ビルド(npm run build)すると dist/ ができます。ターミナルには php-wasm まわりで eval の警告が出ていますが、これはライブラリ側の実装に関するもので、今回書いたコードの不具合ではありません。ビルド自体は成功しているので、試作としてはこのまま進めています。

デプロイは npm run deploy で行っています。これは npm 標準のコマンドではなく、package.json の scripts に自分で書いたものです。中身は以下のとおりで、公開用ビルドのあと、Wrangler(Cloudflare 公式の CLI)で Cloudflare Pages に dist/ を上げる、という流れです。今回の記事では導入手順までは書きませんが、あらかじめ Cloudflare アカウントがあり、npx wrangler login などでログイン済みであることが前提です。
package.json
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=osampo --commit-dirty=true"

公開先でもサーバー側のアプリ PHP は不要で、静的配信+必要最低限の Functions(Overpass プロキシ)という構成です。
6)実装のツボ
試作で意識した点などを、いくつか挙げます。
⚪︎JS と PHP、なぜ分ける?
DOM・GPS・カメラ・地図操作はブラウザ API なので JavaScript 側が自然です。一方、距離計算・100m ごとの判定・POI 接近・ログ整形は、普段使っている PHP で書きたい処理でした。そこで「通信と UI は JS、判定とログは PHP」と分けています。
⚪︎位置情報は HTTPS 必須(ハマりどころ)
ブラウザの Geolocation API は、セキュアなコンテキスト(HTTPS など)が前提です。ローカルは前述の Vite+basic-ssl、公開先は Cloudflare Pages が付与する HTTPS に任せています。HTTP のまま起動すると、地図は出ても現在地だけ取れない、という状態になるので注意です。
⚪︎php-wasm から Overpass は直接呼べない
普段のサーバー上の PHP なら、curl などで Overpass API を直接呼べます。一方、ブラウザ内の php-wasm は、サーバーの PHP と同じようには外向けの HTTP 通信が使えません。ブラウザから外の API を呼ぶのは、基本的に JavaScript の仕事だからです。
そのため Overpass への問い合わせは JavaScript の fetch で行い、取ってきた結果を PHP に渡して判定させています。
公開時は、ブラウザから Overpass へ直接問い合わせると CORS で弾かれる場合があるため、いったん自分のサイトの /api/overpass(Cloudflare Pages Functions)へ問い合わせ、そこから Overpass に取りに行く形にしています。同じサイト経由にすることで、端末ごとの通信事情に左右されにくくなります。ローカル開発環境では、同じ役割を Vite のプロキシが担います。
⚪︎WASM の初回ロードと容量制限
php-wasm の本体(.wasm)はおよそ 18MB あり、初回の読み込みに数秒かかることがあります。回線が遅い環境では、起動の待ち時間が発生しやすいです。
加えて、Cloudflare Pages には 1 ファイルあたり 25 MiB という上限があります。現状の約 18MB なら載せられますが、将来ランタイムが大きくなって超えるようなら、WASM だけ R2(Cloudflare のオブジェクトストレージ)に別置きにする、といった逃げ道も必要になりそうです。
7)まとめ
今回の試作で分かったことは、以下の点でした。
- ブラウザ上でも PHP ランタイムを載せれば、普段の PHP の感覚でロジックを書ける
- ただし「全部 PHP」にはならず、ブラウザ API や外部通信は JavaScript 側の仕事になる
- 公開は静的配信(Cloudflare Pages)で足りた。サーバー側のアプリ PHP は不要だった
- 初回の WASM 読み込みや、Overpass の遅さ・制限は、使い勝手に響く
実装の上で肝となる点として印象に残ったのは、WASM の初回ロード、位置情報に HTTPS が必要なこと、Overpass の安定性あたりでした。
試作については「ブラウザ PHP を触ってみる」ことが主目的だったので、まずは一通り動くところまでできたのが収穫でした。
※ 記事中に公開 URL へのリンクを張っていますが、こちら側の都合で見えなくなる(公開を止める)可能性があります。あらかじめご了承ください。