JavaScript | Web API:ファイル・データ操作 - 大容量ファイルの扱い

JavaScript JavaScript
スポンサーリンク

「大容量ファイルの扱い」は“全部一気にやらない”が合言葉

まず前提からいきます。

数 KB〜数 MB 程度のファイルなら、
FileReader.readAsTextreadAsArrayBuffer
一気に読み込んでも、たいてい問題なく動きます。

でも、数百 MB〜数 GB クラスになってくると、

  • 読み込みに時間がかかる
  • メモリを大量に消費する
  • ブラウザが固まったように見える

といった問題が一気に表面化します。

大容量ファイルを扱うときのキーワードは、
「分割」「ストリーミング」「UI を固めない」 の 3 つです。

ここでは、初心者向けに

  • なぜ一気読みが危険なのか
  • slice を使った分割読み込み
  • 進捗表示の考え方
  • 大容量アップロードの基本パターン

を、例題付きでかみ砕いていきます。


なぜ「大容量ファイルを一気に読む」のが危険なのか

メモリを一気に食う

FileReader.readAsText(file)readAsArrayBuffer(file) は、
ファイル全体をメモリに展開します。

例えば 1GB のファイルを readAsArrayBuffer すると、
その 1GB 分のメモリを一気に確保することになります。

ブラウザは他にもいろいろメモリを使っているので、
これが原因で

  • ものすごく重くなる
  • タブがクラッシュする
  • OS 全体が重くなる

といったことが普通に起こります。

UI が「固まったように見える」

大きな処理をメインスレッドで一気にやると、
その間ブラウザは他のこと(描画やイベント処理)ができません。

ユーザーから見ると、

  • ボタンを押しても反応しない
  • スクロールできない
  • ぐるぐるマークのまま止まっているように見える

という「フリーズした感」が出ます。

これを避けるために、

  • ファイルを「少しずつ」読む
  • 読み込みの合間に UI を更新する
  • 必要なら Web Worker に処理を逃がす

といった工夫が必要になります。


File / Blob の slice を使って「分割読み込み」する

File.slice で一部だけ切り出す

File(や Blob)には slice というメソッドがあります。

const chunk = file.slice(start, end);
JavaScript

startend はバイト位置です。

例えば、1MB ずつ読みたいなら、

const chunkSize = 1024 * 1024; // 1MB
const firstChunk = file.slice(0, chunkSize);
const secondChunk = file.slice(chunkSize, chunkSize * 2);
JavaScript

のように、ファイルを「チャンク(塊)」に分割して扱えます。

例題:テキストファイルを 1MB ずつ読みながら処理するイメージ

HTML:

<input type="file" id="fileInput" accept=".txt">
<pre id="log" style="max-height:200px;overflow:auto;"></pre>

JavaScript(ざっくりしたイメージ):

const input = document.querySelector("#fileInput");
const log = document.querySelector("#log");

input.addEventListener("change", () => {
  const file = input.files[0];
  if (!file) return;

  const chunkSize = 1024 * 1024; // 1MB
  let offset = 0;

  function readNextChunk() {
    if (offset >= file.size) {
      appendLog("完了");
      return;
    }

    const slice = file.slice(offset, offset + chunkSize);
    const reader = new FileReader();

    reader.addEventListener("load", () => {
      const text = reader.result;
      appendLog(`オフセット ${offset} から ${text.length} 文字読み込み`);

      offset += chunkSize;

      setTimeout(readNextChunk, 0);
    });

    reader.readAsText(slice, "utf-8");
  }

  readNextChunk();
});

function appendLog(message) {
  log.textContent += message + "\n";
}
JavaScript

ここでの重要ポイントは、

  • file.slice で一部だけを切り出している
  • 読み込みが終わるたびに offset を進めて次のチャンクを読む
  • setTimeout(..., 0) を挟んで、UI 更新のチャンスをブラウザに渡している

というところです。

これにより、

  • 一度にメモリに載るのは 1MB 分だけ
  • 読み込みの合間に画面が更新されるので「固まった感」が減る

という効果が得られます。


大容量アップロードの基本:チャンクに分けて順番に送る

一気に送ると何が問題か

大きなファイルを fetchXMLHttpRequest で一気に送ると、

  • ネットワーク的に時間がかかる
  • 途中で切れたときに「最初からやり直し」になる
  • サーバー側も一度に大きなボディを受け取る必要がある

といった問題が出てきます。

そこでよく使われるのが、
「チャンクアップロード」 という手法です。

ファイルを一定サイズのチャンクに分けて、
それぞれを順番にサーバーに送っていきます。

例題:ファイルを 1MB ごとに分割して順番に送るイメージ

サーバー側の実装はここでは省略して、
クライアント側の流れだけイメージしましょう。

async function uploadInChunks(file) {
  const chunkSize = 1024 * 1024; // 1MB
  let offset = 0;
  let index = 0;

  while (offset < file.size) {
    const slice = file.slice(offset, offset + chunkSize);

    const formData = new FormData();
    formData.append("chunk", slice);
    formData.append("index", String(index));
    formData.append("totalSize", String(file.size));

    await fetch("/upload-chunk", {
      method: "POST",
      body: formData
    });

    offset += chunkSize;
    index++;
  }

  await fetch("/upload-complete", {
    method: "POST",
    body: JSON.stringify({ totalSize: file.size }),
    headers: { "Content-Type": "application/json" }
  });
}
JavaScript

ここでのポイントは、

  • file.slice でチャンクを切り出している
  • 各チャンクを FormData に入れてサーバーに送っている
  • 最後に「全部送ったよ」という通知を別エンドポイントに送っている

という構成です。

実際には、

  • 途中で失敗したチャンクのリトライ
  • どこまで送ったかの再開(レジューム)
  • サーバー側でのチャンク結合

など、もう少し複雑になりますが、
「大きなファイルを小さな塊に分けて送る」という発想が大事です。


進捗表示(プログレスバー)の考え方

どこまで処理が進んだかを「割合」で出す

大容量ファイルを扱うとき、
ユーザーに「今どれくらい進んでいるか」を見せるのはとても重要です。

チャンク読み込みやチャンクアップロードをしているなら、

  • 今までに処理したバイト数
  • 全体のファイルサイズ

から、進捗率を計算できます。

const progress = processedBytes / file.size; // 0〜1
JavaScript

これをパーセンテージにして表示します。

const percent = Math.floor(progress * 100);
progressBar.style.width = percent + "%";
progressText.textContent = percent + "%";
JavaScript

例題:チャンク読み込みしながら進捗バーを更新する

HTML:

<input type="file" id="fileInput">
<div style="width:300px;height:10px;border:1px solid #ccc;margin-top:8px;">
  <div id="bar" style="height:100%;width:0;background:#4caf50;"></div>
</div>
<span id="percent">0%</span>

JavaScript(読み込み部分だけ抜粋):

const input = document.querySelector("#fileInput");
const bar = document.querySelector("#bar");
const percentText = document.querySelector("#percent");

input.addEventListener("change", () => {
  const file = input.files[0];
  if (!file) return;

  const chunkSize = 1024 * 1024; // 1MB
  let offset = 0;

  function updateProgress() {
    const progress = offset / file.size;
    const percent = Math.floor(progress * 100);
    bar.style.width = percent + "%";
    percentText.textContent = percent + "%";
  }

  function readNextChunk() {
    if (offset >= file.size) {
      updateProgress();
      return;
    }

    const slice = file.slice(offset, offset + chunkSize);
    const reader = new FileReader();

    reader.addEventListener("load", () => {
      // ここで slice の中身を処理する(今回は省略)

      offset += chunkSize;
      updateProgress();

      setTimeout(readNextChunk, 0);
    });

    reader.readAsArrayBuffer(slice);
  }

  readNextChunk();
});
JavaScript

ここでの重要ポイントは、

  • offset を「今までに処理したバイト数」として使っている
  • offset / file.size で進捗率を出している
  • チャンクを処理するたびにバーを更新している

というところです。

これだけでも、
「何も表示されないで待たされる」状態から
「ちゃんと進んでいる感がある」状態に変わります。


大容量ファイルを扱うときの「考え方のスイッチ」

最後に、考え方の話を少し。

小さなファイルなら、

  • 一気に読み込む
  • 一気に送る
  • 一気に処理する

で済みます。

でも、大容量ファイルでは、
この「一気に」が全部敵になります。

そこで、発想をこう切り替えます。

  • 「全部」ではなく「少しずつ」
  • 「一度に」ではなく「チャンクごとに」
  • 「終わるまで待つ」ではなく「進捗を見せながら進める」

技術的には、

  • File.slice で分割
  • FileReader.readAsArrayBuffer / readAsText をチャンクごとに呼ぶ
  • fetch でチャンクごとにアップロード
  • 進捗率を計算して UI を更新

というパターンを組み合わせていきます。

まずは次の 2 つを、自分の手で書いてみてください。

  • 大きめのテキストファイルを 1MB ずつ読みながら、進捗バーを更新するコード
  • ファイルを 1MB ごとに分割して、ダミーのエンドポイントに順番に送るコード(実際のサーバー処理は仮で OK)

これが書けるようになったとき、
「大容量ファイルの扱い」は単なる“重そうなテーマ”ではなく、
「データをどう分割し、どう見せながら処理するかを設計する面白い領域」
として見えてくるはずです。

タイトルとURLをコピーしました