HTMLファイルをダブルクリックすると、file:///C:/tools/index.htmlのようなURLでブラウザが開きます。同じファイルをローカルサーバーから配信すると、http://localhost:8080/のようなURLになります。厳密にはfile:はURL scheme、localhostはhostnameなので、この記事ではfile://で直接開く場合とhttp://localhostで配信する場合を比較します。中身が同じHTMLなら、どちらでも同じように動きそうです。

ところが実際には、file://ではJavaScript Moduleの読み込みや隣のファイルへのfetchが失敗し、localhostでは動くことがあります。逆に、単一HTMLとして直接開けること自体が大きな利点になる場面もあります。Browser Kittyではこの2つを別の実行環境として確認しています。違いの中心にあるのは、ファイルが同じかどうかではなく、ブラウザがそのページをどのURL・Origin・HTTP条件で扱うかです。

まず分けて考えたい4つのこと

file://

ファイルを直接読む

Webサーバーを介さず、ブラウザが端末内のHTMLを直接開きます。HTTPレスポンスは存在せず、file URL特有のOriginや保存領域の扱いを受けます。

localhost

HTTPで自分の端末から受け取る

ローカルWebサーバーへHTTPリクエストを送り、レスポンスとしてHTMLを受け取ります。通常のOriginの扱いに加え、Content-TypeやRange関連、CSPなどをHTTPレスポンスヘッダーとして設定できます。

Secure Context

安全なコンテキストかは別軸

localhostは開発用に信頼できるOriginとして扱われ、file URLも潜在的に信頼できるものとして扱われます。ただし、Secure Contextなら全APIが使えるという意味ではありません。

Local processing

ローカル処理かどうかも別軸

file://でも、CORSなど相手側の条件を満たせば外部APIへ通信できますし、localhostでも外部通信なしで処理できます。完全ローカル処理かどうかはURLではなく、実際のデータ経路で判断します。

同じHTMLでも、ブラウザから見た実行環境は同じではない

問題を短くすると、HTMLの中身ではなく、そのHTMLへどう到達したかによってブラウザが適用するルールが変わります。file://ではファイルシステム上のファイルを直接開き、localhostではWebサーバーからHTTPレスポンスとして受け取ります。

その違いから、Origin、CORS、HTTPヘッダー、Web APIの利用条件、保存領域などに差が生まれます。コードを一切変更していなくても、開き方だけで動作差が出るのはこのためです。

同じHTMLから動作差が生まれるまで(概念図)
同じ index.html file:// / localhost Origin・HTTP条件 CORS・API条件 動作差

HTMLが同じでも、URL schemeと配信方法が変わればブラウザが適用する条件も変わる

file://は直接読み込み、localhostはHTTP配信

file://で開く場合、間にWebサーバーはいません。HTML、JavaScript、画像などはローカルファイルとしてブラウザから参照されます。localhostではブラウザが自分の端末上で動くWebサーバーへHTTPリクエストを送り、HTTPレスポンスとしてリソースを受け取ります。

localhostという名前が付いていても、それだけでクラウドへアップロードしているわけではありません。localhostは通常ループバック先として同じ端末を指します。ただし、ページ内のコードが別の外部URLへ通信することはできるため、『localhostだから通信しない』とも言えません。

同じHTMLを開く2つの方法
観点file://http://localhost
読み込みファイルを直接読むローカルWebサーバーからHTTPで受け取る
Origin通常opaque originとして扱われるscheme・host・portで決まる通常のOrigin
HTTPレスポンスなしあり
レスポンスヘッダーHTTPレスポンス自体がないため、HTTPレスポンスヘッダーもないCSP、Content-Type、Range関連などをHTTPレスポンスヘッダーとして返せる
単一HTMLダブルクリックで使える構成に向くWeb配信としての挙動確認に向く

大きな違いの一つがOrigin

通常のWebページでは、Originはおおまかにscheme・hostname・portの組み合わせで決まります。たとえばhttp://localhost:8080から配信されたページと同じOriginのリソースなら、同一Originとして扱いやすくなります。

file URLは特殊です。MDNでは、現在のブラウザはfile:///で読み込まれたファイルを通常opaque originとして扱うと説明しています。opaque originは他のOriginと同一とは判定されないため、同じフォルダにある別ファイルでも自動的に同一Originとはみなされません。なお、file URLのOriginの扱いには実装依存の部分もあります。

JavaScript Moduleやfetchで差が出やすい

分かりやすい例がJavaScript Moduleです。HTMLから別ファイルのModuleを読み込む構成をfile://で試すと、Moduleのセキュリティ要件によってCORSエラーになることがあります。MDNも、Moduleをローカルファイルとして試すのではなくサーバー経由でテストするよう案内しています。

fetchで隣のJSONやテキストを読む構成でも同様です。ファイルが同じフォルダに存在していても、それだけでJavaScriptから自由に読めるとは限りません。localhostなら、そのファイルを同じHTTP Originからレスポンスとして返す構成にできます。

localhostにはHTTPレスポンスがある

localhostではWebサーバーが応答するため、Content-Type、Content-Length、Content-Range、Content-Security-Policy、Cross-Origin-Opener-Policy、Cross-Origin-Embedder-Policyなど、用途に応じたHTTPヘッダーを返せます。file://にはサーバーから返されるHTTPレスポンスそのものがありません。

メディア再生ではこの違いが具体的に現れます。HTTP Range Requestは、動画プレーヤーなどが大きなリソースの必要な範囲だけ取得するために使えます。Browser Kittyのローカルプレビューサーバーでも紹介動画のRange Requestを処理しています。実装中には、シークなどで古いRange接続をブラウザが閉じたあと、サーバーがその接続へ書き込もうとして例外になる問題もありました。HTMLだけでなく、Webサーバー側の振る舞いもlocalhost環境の一部です。

Secure Contextだけでは説明できない

『file://は安全ではないから動かず、localhostは安全だから動く』と整理すると不正確です。Secure Contextの仕様ではlocalhostのようなループバック先は開発環境として潜在的に信頼できるOriginとして扱われます。またfile schemeも潜在的に信頼できる対象として扱われます。

つまり、file://とlocalhostの違いをSecure Contextだけで説明することはできません。実際には、Origin、URL scheme、CORS、API固有の条件、権限、ユーザー操作などを分けて見る必要があります。

Secure Contextでも、file://では使えないAPIがある

Service Workerが分かりやすい例です。Service WorkerはSecure Context向けの機能ですが、register()へ渡すscript URLやscope URLにはさらに条件があり、http:またはhttps:以外のschemeはTypeErrorになるとMDNに記載されています。localhostはローカル開発用のSecure ContextとしてService Workerを利用できますが、file://のHTMLからfile:のService Workerをそのまま登録することはできません。

このように、『Secure Contextである』はWeb API利用条件の一部にすぎません。ある機能がfile://で動くかを判断するときは、そのAPI固有の仕様まで確認する必要があります。

localStorageもfile://では同じ前提にできない

通常のWebサイトではlocalStorageはOriginごとに分かれます。http://localhost:8080で開くページなら、そのOriginに対応する保存領域として考えられます。

一方、MDNではfile: URLから読み込んだ文書のlocalStorageの要件は未定義で、ブラウザによって挙動が変わり得ると説明しています。現在はfile URLごとに別の保存領域のように見えるブラウザがあっても、それを将来も保証される仕様として依存するべきではありません。単一HTMLで設定や作業状態を保存する場合は、この差も考慮が必要です。

だからBrowser Kittyではfile://とlocalhostを別々に確認する

Browser Kittyでは、可能なツールを単一HTMLとして配布できる構成にしています。JavaScript、CSS、SVG、Worker、Wasmなどを可能な範囲で1ファイルへまとめれば、隣のファイルをfetchしたりModuleとして読み込んだりする必要が減り、file://でも成立しやすくなります。これは配布しやすさだけでなく、ローカルファイル特有の制約を減らす意味もあります。

一方、開発時にはlocalhostでも確認します。Web版としてのMIME type、Range Request、CSP・COOP・COEPなどをHTTPレスポンスヘッダーとして設定した場合の挙動など、HTTP配信して初めて確認できる部分があるからです。単一HTMLで動いたからWeb版も同じ、localhostで動いたからダブルクリックでも同じ、とは限りません。

同じHTMLなのに動作が違うときは、コードだけでなくアドレスバーも確認します。file://なのか、http://localhostなのか、公開されたhttps://なのか。Webアプリでは、その違いも実行環境の一部です。

Browser Kittyで試す

Developer Toolbox

Base64、JSON、JWT、cron、正規表現、Hashなど33種類の開発者向けユーティリティを1つにまとめたツールです。

ツールを開くツールの詳細を見る

注意点

  • 『file://で開ける』と『完全ローカル処理』は同じ意味ではありません。file://のHTMLでも、CORSなど相手側の条件を満たせば外部URLへ通信するコードを実行できます。
  • localhostは通常自分の端末へ戻るループバックですが、ページ内の外部通信まで止める仕組みではありません。NetworkパネルやCSPなどで別に確認します。
  • 単一HTML化で別ファイル読み込みの問題は減らせますが、API自体がhttp: / https:や特定のOrigin条件を要求する場合まで回避できるわけではありません。

よくある質問

localhostを使うと、ファイルはインターネットへ送られますか?

localhost上のローカルWebサーバーからHTMLを配信すること自体は、通常その端末内のループバック通信です。ただし、ページ内のJavaScriptが外部APIや外部リソースへ通信すれば、その通信は別に発生します。

file://なら完全ローカル処理ですか?

いいえ。file://はHTMLをローカルファイルから開いていることを示しますが、そのページが外部へ通信しないことまでは保証しません。完全ローカル処理かどうかは、ユーザーデータと実行時通信の実際の経路で判断します。

file://もSecure Contextなら、なぜService Workerが使えないのですか?

Secure Contextは条件の一つだからです。ServiceWorkerContainer.register()にはさらにURL schemeの条件があり、script URLやscope URLはhttp:またはhttps:である必要があります。localhostは開発用の信頼できるHTTP Originとして扱えます。

単一HTMLなら必ずfile://で動きますか?

必ずではありません。別ファイルへのfetchやimportを減らせるので動きやすくなりますが、利用するWeb API自体にOrigin、scheme、権限、ユーザー操作などの条件があれば、その制約は残ります。

開発ではfile://とlocalhostのどちらを使えばよいですか?

Webサイトとして動かすものはlocalhostで確認するのが基本です。ダウンロードして直接開く単一HTMLも提供するなら、localhostだけでなくfile://でも主要機能を別途確認するのが安全です。

参考資料

file URLのOrigin、JavaScript Module、Secure Context、Service Worker、localStorage、HTTP Range Requestについては、以下の仕様・公式ドキュメントを参照しています。Browser Kitty固有の記述は、v168時点のローカル開発サーバーと単一HTML運用で実際に確認した内容です。