3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザの「Origin Private File System(OPFS)」を使ってローカルでのデータ読み書き(ローカルにデータ保持)

Last updated at Posted at 2025-10-17

はじめに

この記事は、「OPFS(Origin Private File System/オリジンプライベートファイルシステム)という、ブラウザのローカルにデータを保存する仕組みに関する内容です。

以下に、MDN での OPFS に関するページや説明を少し掲載します。

●オリジンプライベートファイルシステム - Web API | MDN
 https://developer.mozilla.org/ja/docs/Web/API/File_System_API/Origin_private_file_system

2025-10-14_14-19-33.jpg

以下、少し長いですが、上記の MDN からの一部抜粋です。

【MDN での記載の抜粋】
オリジンプライベートファイルシステム (OPFS) は、ファイルシステム API の一部として提供されるストレージエンドポイントです。これは、パフォーマンスのために高度に最適化され、その内容へのその場での書き込みアクセスを提供する特別な種類のファイルへのアクセスを提供します。

OPFS は低レベルのバイト単位のファイルアクセスを提供し、ページのオリジンには非公開でユーザーには見えません。そのため、一連のセキュリティチェックや 権限付与を必要とせず、ファイルシステムアクセス API を呼び出すよりも高速です。また、メインスレッドをブロックしないようにウェブワーカーの中だけで実行できる一連の同期呼び出しも利用できます(他のファイルシステム API 呼び出しは非同期です)。

OPFS がユーザーから見えるファイルシステムと異なる点をまとめると、次のようになります。

  • OPFS は、他のオリジン分割されたストレージメカニズム(IndexedDB API など)と同様に、ブラウザーストレージ容量制限の対象となります。 OPFS が使用しているストレージ空間の大きさは navigator.storage.estimate() で知ることができます。
  • サイトのストレージデータをクリアすると、 OPFS が削除されます。
  • OPFS のファイルにアクセスするために、その許可のプロンプトやセキュリティチェックは要求されません。
  • ブラウザーは OPFS の内容をディスクのどこかに保持しますが、作成したファイルを 1 対 1 で照合して探すことはできません。 OPFS はユーザーから見えることを意図していません。

この後で少し OPFS についての話と、類似した仕組みとの違いについて書いてみます。

OPFS について

ブラウザでローカルにデータを保持する仕組みとして、元からローカルストレージや、他のいくつかの仕組みがありました。OPFS もブラウザ上で動作するアプリが、ローカルにデータを保持する仕組みです。

OPFS の概要

OPFS は、上記の MDN の説明にもあるように、自分専用のプライベートなファイルシステムに直接アクセスするようなものです。OPFS の特徴について、上記の MDN の説明の抜粋と一部重複する部分もありますが、以下にリストで少し書いてみます。

  • ローカルデータへのアクセスについては、その OPFS を作成した Webサイト(オリジン)のアプリのみアクセス可能な状態となり、他の Webサイトやユーザーの PC のファイルシステムからは完全に隔離される
  • ファイルやディレクトリ(フォルダ)の概念があり、階層構造でのデータ管理が可能
  • 一度、保存したデータの一部だけを編集/変更して保存するというのが扱いやすい
  • ブラウザを閉じたり、PCをシャットダウンしてもデータは保持される(※ ただし、他のローカルストレージなどと同様、確実な保持は保証されません)

ローカルにデータを保存する他の仕組み(一部)

OPFSが登場するまで、Webアプリケーションがクライアントサイドにデータを保存する方法はいくつか存在しました。

例えば「ローカルストレージ(LocalStorage)やセッションストレージ(SessionStorage)があります。これらは、キーと値のペアで文字列データを保存するシンプルな仕組みです。シンプルで便利な一方で、容量の制限が厳しかったり、保存できるのが文字列のみ(画像や動画などのバイナリデータを直接保存できない ← Base64エンコードすれば可能ではあるものの、効率が悪い)となっています。そして、ファイルやディレクトリの概念がないため、複雑なデータ構造の管理は難しいです。

他の仕組みで IndexedDB があります。こちらは、ブラウザに組み込まれた NoSQLデータベースであり、大量の構造化データを保存・検索できます。こちらも、一般的なファイル操作(例えば、ファイルを読み込んで部分的に変更し、再度保存する)には向いていません。それと、操作が複雑になりがちです。

実際に試してみる

それでは、実際に簡単なサンプルで OPFS を試してみます。以下の MDN の内容も見つつお試し用のコードを用意します。

●オリジンプライベートファイルシステム - Web API | MDN
 https://developer.mozilla.org/ja/docs/Web/API/File_System_API/Origin_private_file_system

試す内容(概要)

試しに用意するサンプルの機能・内容は、おおまかには以下の通りです。

  • ボタン押下で OPFS に文字列を保存できる
  • ボタン押下で OPFS に保存済みの内容を読み出せる
  • ボタン押下で OPFS の中身をクリアできる

お試し用のコード1

以下に、お試し用のコードを示します。まずはシンプル版です。

<!DOCTYPE html>
<meta charset="utf-8" />
<title>OPFS: シンプルな事例で store.json を利用</title>

<h1>OPFS(単一ファイル JSON)</h1>

<section>
  count: <input id="count" type="number" step="any" value="123" /> note:
  <input id="note" value="OPFS に保存" /><br />
  <button id="save">保存</button>
  <button id="load">読み込み</button>
  <button id="reset">内容を空にする</button>
</section>

<pre id="log"></pre>

<script>
  (async () => {
    const logEl = document.getElementById("log");
    const log = (m) => (logEl.textContent += m + "\n");

    if (!("storage" in navigator) || !navigator.storage.getDirectory) {
      log(
        "OPFS (navigator.storage.getDirectory) をサポートしていないブラウザです"
      );
      return;
    }

    async function saveStore(obj) {
      const root = await navigator.storage.getDirectory();
      const fh = await root.getFileHandle("store.json", { create: true });
      const w = await fh.createWritable();
      await w.write(JSON.stringify(obj));
      await w.close();
    }

    async function loadStore(fallback = { count: 0, note: "" }) {
      try {
        const root = await navigator.storage.getDirectory();
        const fh = await root.getFileHandle("store.json");
        const file = await fh.getFile();
        return JSON.parse(await file.text());
      } catch {
        return fallback;
      }
    }

    document.getElementById("save").onclick = async () => {
      const obj = {
        count: Number(document.getElementById("count").value),
        note: document.getElementById("note").value,
      };
      await saveStore(obj);
      log("saveStore_保存完了: " + JSON.stringify(obj));
    };

    document.getElementById("load").onclick = async () => {
      const obj = await loadStore();
      document.getElementById("count").value = obj.count;
      document.getElementById("note").value = obj.note;
      log("loadStore_内容を取得: " + JSON.stringify(obj));
    };

    document.getElementById("reset").onclick = async () => {
      await saveStore({});
      log("store.json を空にしました");
    };

    log("準備OK(HTTPS または http://localhost で実行してください)");
  })();
</script>

ローカルサーバーを用意して上記を開くと、以下のページが開きます。

2025-10-17_22-13-18.jpg

count の横と、note の横が、文字列の入力欄です。

それらの入力欄の下のボタン 3つが、上で書いていたシンプルな機能を実行するボタンです。

お試し用のコード2

先ほどのコードに対して、「ページを開いた時に、もし OPFS に保存済みの内容があれば、それを自動で読み込む」という機能を付け足してみます。

その機能をマージした後の機能は、以下の通りです。

<!DOCTYPE html>
<meta charset="utf-8" />
<title>OPFS: シンプルな事例で store.json を利用(最初は自動読み込み)</title>

<h1>OPFS(単一ファイル JSON、最初は自動読み込み)</h1>

<section>
  count: <input id="count" type="number" step="any" value="123" /> note:
  <input id="note" value="OPFS に保存" /><br />
  <button id="save">保存</button>
  <button id="load">読み込み</button>
  <button id="reset">内容を空にする</button>
</section>

<pre id="log"></pre>

<script>
  (async () => {
    const logEl = document.getElementById("log");
    const countEl = document.getElementById("count");
    const noteEl = document.getElementById("note");
    const log = (m) => (logEl.textContent += m + "\n");

    if (!("storage" in navigator) || !navigator.storage.getDirectory) {
      log(
        "OPFS (navigator.storage.getDirectory) をサポートしていないブラウザです"
      );
      return;
    }

    async function saveStore(obj) {
      const root = await navigator.storage.getDirectory();
      const fh = await root.getFileHandle("store.json", { create: true });
      const w = await fh.createWritable();
      await w.write(JSON.stringify(obj));
      await w.close();
    }

    async function loadStore(fallback = { count: 0, note: "" }) {
      try {
        const root = await navigator.storage.getDirectory();
        const fh = await root.getFileHandle("store.json");
        const file = await fh.getFile();
        return JSON.parse(await file.text());
      } catch {
        return fallback;
      }
    }

    function applyToUI(obj) {
      countEl.value = obj.count ?? 0;
      noteEl.value = obj.note ?? "";
    }

    // --- 自動ロード(ページ読み込み時) ---
    const initial = await loadStore();
    applyToUI(initial);
    log("自動ロード: " + JSON.stringify(initial));

    document.getElementById("save").onclick = async () => {
      const obj = { count: Number(countEl.value), note: noteEl.value };
      await saveStore(obj);
      log("saveStore_保存完了: " + JSON.stringify(obj));
    };

    document.getElementById("load").onclick = async () => {
      const obj = await loadStore();
      applyToUI(obj);
      log("loadStore_内容を取得: " + JSON.stringify(obj));
    };

    document.getElementById("reset").onclick = async () => {
      await saveStore({});
      applyToUI({ count: 0, note: "" });
      log("store.json を空にしました");
    };

    log("準備OK(HTTPS または http://localhost で実行してください)");
  })();
</script>

ローカルサーバーを用意して上記を開くと、今度は最初に、OPFS の中身を自動ロードします。

2025-10-17_22-16-09.jpg

おわりに

今回、とりあえず「Origin Private File System(OPFS)」を試してみました。

今の内容だと、OPFS以外の類似の仕組みでも簡単に実現できる内容だったので、別途、OPFS ならではの内容も試せればと思います。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?