「大容量ファイルの扱い」は“全部一気にやらない”が合言葉
まず前提からいきます。
数 KB〜数 MB 程度のファイルなら、FileReader.readAsText や readAsArrayBuffer で
一気に読み込んでも、たいてい問題なく動きます。
でも、数百 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);
JavaScriptstart と end はバイト位置です。
例えば、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 分だけ
- 読み込みの合間に画面が更新されるので「固まった感」が減る
という効果が得られます。
大容量アップロードの基本:チャンクに分けて順番に送る
一気に送ると何が問題か
大きなファイルを fetch や XMLHttpRequest で一気に送ると、
- ネットワーク的に時間がかかる
- 途中で切れたときに「最初からやり直し」になる
- サーバー側も一度に大きなボディを受け取る必要がある
といった問題が出てきます。
そこでよく使われるのが、
「チャンクアップロード」 という手法です。
ファイルを一定サイズのチャンクに分けて、
それぞれを順番にサーバーに送っていきます。
例題:ファイルを 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)
これが書けるようになったとき、
「大容量ファイルの扱い」は単なる“重そうなテーマ”ではなく、
「データをどう分割し、どう見せながら処理するかを設計する面白い領域」
として見えてくるはずです。

