代表的な面接トピック

フロントエンド面接:Compression Streams を使って大容量ファイルを安全に処理するにはどうすればよいか?

フロントエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

ブラウザにおいて、低メモリ消費、キャンセル対応、そして悪意のある入力からの保護を担保しつつ、大容量アップロードの gzip 圧縮とダウンロードの伸長を行う必要があります。ストリームパイプラインを設計し、フォーマット、バックプレッシャー、エラー処理、セキュリティ境界について説明してください。

プロンプトとコンテキスト

ブラウザにおいて、低メモリ消費、キャンセル対応、そして悪意のある入力からの保護を担保しつつ、大容量アップロードの gzip 圧縮とダウンロードの伸長を行う必要があります。ストリームパイプラインを設計し、フォーマット、バックプレッシャー、エラー処理、セキュリティ境界について説明してください。

Compression Streams API は、Web Streams パイプライン内でバイナリチャンクを扱うための CompressionStreamDecompressionStream を提供します。標準仕様では brotlideflatedeflate-rawgzip が定義されています。この API は変換処理を提供するものであり、サイズ制限、認証、ビジネス上の整合性チェックを提供するものではありません。

面接官がテストしていること

Readable/Writable/TransformStream の合成、バックプレッシャーとキュー、フラッシュ処理、フォーマットの境界、伸長エラー、キャンセル伝播、メモリバジェット、伸長爆弾および長さサイドチャネル攻撃、プログレッシブエンハンスメント、サーバーとのネゴシエーションを網羅できているかを確認しています。

30秒の回答フレームワーク

「Blob ストリームまたはレスポンスボディを pipeThrough 経由で CompressionStream に接続してバックプレッシャーを維持し、AbortSignal でキャンセルを伝播させます。ダウンロード時は、DecompressionStream をパーサーに渡す前に圧縮サイズ、伸長後バイト数、処理時間を制限し、フォーマットや整合性のエラー発生時には中断します。ブラウザがこの機能を備えていない場合は、サーバー側での圧縮にフォールバックします。この API はセキュリティスキャナーや整合性検証ツールではありません。」

ステップごとの詳細解説

ステップ 1: 入力ストリームと出力ストリームを選択する

アップロードは Blob.stream() から、ダウンロードは Response.body から開始できます。CompressionStream は fetch のボディ、ファイルシンク、またはパーサー向けに ReadableStream を生成します。最初にファイル全体を ArrayBuffer に読み込んではいけません。

ステップ 2: 変換ストリームを接続する

最小限のアップロードパイプラインは以下のとおりです:

javascript
async function uploadGzip(blob, signal) {
  const compressed = blob.stream().pipeThrough(
    new CompressionStream("gzip"),
    { signal },
  );
  return fetch("/upload", {
    method: "POST",
    body: compressed,
    signal,
    headers: { "Content-Encoding": "gzip" },
  });
}

実際のプロトコルでは、リクエスト長、リトライセマンティクス、およびサーバーがストリーミングリクエストボディを受け入れるかどうかを定義する必要があります。

ステップ 3: バックプレッシャーとフラッシュを理解する

Streams API は、下流のキューおよび書き込み速度に応じて上流の読み取り速度を調整します。すべてのチャンクを無制限の配列に追加してはいけません。低速なシンクに対しては、パイプラインが自然に一時停止する必要があります。書き込み側をクローズすることで、最終的な圧縮ブロックとチェックサムが確実にフラッシュされなければなりません。

ステップ 4: フォーマットをネゴシエーションする

コンストラクタはサポートされているフォーマット文字列を受け入れ、未サポートの文字列に対しては例外をスローします。クライアントとサーバーは Content-Encoding またはビジネス固有のフィールドについて合意する必要があります。deflatedeflate-rawgzip はラッパーが異なるため、事前のネゴシエーションなしに Brotli を送信してはいけません。

ステップ 5: 伸長処理に制限を設ける

圧縮された入力は、極めて巨大な出力へと展開される可能性があります。出力バイト数、ファイルエントリ数、処理時間、並行ジョブ数をカウントし、バジェットを超過した場合はキャンセルまたは中断します。伸長されたコンテンツには、依然として MIME タイプ、パス、コンテンツセキュリティのチェックが必要です。変換が成功したからといって、そのコンテンツが信頼できるわけではありません。

ステップ 6: エラーとキャンセルを伝播させる

フォーマットやチェックサムの検証に失敗すると、TransformStream はエラー状態になります。pipeTo、fetch、およびリーダーからのエラーをキャッチし、ユーザーによるキャンセルをリクエスト、リーダー、ライター、および変換ストリームへ伝播させます。未完了の読み取りが残らないよう、一時的な Blob、ロック、UI の進捗状態を解放します。

ステップ 7: プライバシーと整合性を保護する

圧縮後の長さによって、秘密情報と攻撃者が制御するテキストとの相関関係が明らかになる可能性があります。両者を同一の圧縮コンテキストに含めてはいけません。重要なファイルには独立した署名やハッシュを使用してください。圧縮はバイト列をエンコードするだけであり、送信元の認証や改ざん防止を行うものではありません。

ステップ 8: 互換性とフォールバックを提供する

起動時にコンストラクタとフォーマットをチェックし、メインスレッドを保護するために負荷の高いジョブは Worker で実行します。サポートされていない場合は、サーバー側の圧縮に委譲するか、同一のキャンセル、サイズ、エラーセマンティクスを保ったまま非圧縮でアップロードします。最新のブラウザだけでバリデーションを行わないようにしてください。

トレードオフと境界線

クライアントの CPU vs ネットワーク帯域の節約

圧縮によって転送バイト数は削減されますが、CPU、バッテリー、時間を消費します。モバイル端末では、ファイル形式、ネットワーク品質、バッテリー状態に基づいて判断します。すでに圧縮済みのフォーマットは通常、再圧縮すべきではありません。

ストリーミング vs リトライの簡潔さ

ストリーミングはメモリ消費を抑えられますが、チャンクを認識できるサーバーと冪等なリトライ処理が必要です。再開可能なアップロード(レジューム)では、マルチパートプロトコルに圧縮チャンクを関連付けます。一度消費された ReadableStream を、再度再生可能であるかのようにリトライしてはいけません。

ブラウザ vs サーバーでの伸長

ブラウザでの伸長はサーバーの CPU 負荷を軽減しますが、リソースとセキュリティのバジェット管理がデバイス側に移ります。機密性の高いデータや極端に展開率の高いデータについては、サーバー側で伸長して制限付きの結果を返し、クライアントはそのステータスとチャンクを消費するようにします。

障害訓練と改善計画

シンクの処理が遅くなった場合

アップロード速度をスロットリングし、キューとメモリの推移を確認します。すべてのチャンクが保持され続けていないことを確認してください。それでもキューが増加する場合は、並行数を減らしてパイプラインを一時停止させます。

gzip ストリームが途中で切断された場合

圧縮入力を途中で切断し、フラッシュ時またはチェックサム検証時に適切に失敗すること、UI がリトライ可能な状態になること、リーダー、リクエスト、一時オブジェクトが解放されることを検証します。

出力がバジェットを超過した場合

高展開率のサンプルデータを使用し、バイト制限に達した際に出力全体をメモリ展開・永続化することなく、即座に処理が中断されることを検証します。

よくある間違いとフォローアップ

間違い 1: API が伸長爆弾(decompression bomb)を防いでくれると思い込む

フォローアップ: 何が不足していますか? アプリケーションレベルでのバイト数、時間、エントリ数、並行数のバジェット管理です。API はフォーマット変換のみを行います。

間違い 2: deflate を gzip として扱う

フォローアップ: なぜいけないのですか? ラッパー構造が異なるため、プロトコルのネゴシエーションと適切なフォーマットの一致が必要です。

間違い 3: 消費済みのストリームをリトライする

フォローアップ: 正しい対処法は何ですか? 再生可能な Blob またはチャンクソースからパイプラインを再構築し、サーバーと冪等なアップロード識別子を連携させます。

発展的なフォローアップと模範解答

なぜフラッシュ(flush)が重要なのですか?

入力が終了した際、変換ストリームは末尾の圧縮データ(トレイラー)とチェックサムを出力する必要があります。書き込み側をクローズしないと、消費者は不完全なストリームを受け取ることになります。

長さサイドチャネル攻撃のリスクをどのように軽減しますか?

機密情報と攻撃者が制御するテキストを分離し、圧縮コンテキストの共有を避け、プロトコルパディングを適用するか、それらの相関関係が露出しない設計を採用します。

圧縮処理をサーバー側に残すべきなのはどのような場合ですか?

古いブラウザ、低バッテリーのデバイス、極めて大容量のファイル、または機密データを扱う場合は、サーバー側の処理を使用します。その場合でも伸長後の出力には上限を設け、観測可能な進捗状況をクライアントに提示します。

公開情報ソース

関連する質問