HomeJournalThis post

CompressionStream for Browser Exports

Stream large browser exports through CompressionStream with bounded memory, truthful formats, cancellation, integrity, worker handoff, and server fallbacks.

JP
JP Casabianca
UI/UX designer and full-stack engineer · Bogotá

CompressionStream can compress export bytes as they flow, which avoids building both a giant uncompressed string and a giant compressed copy in browser memory.

This tutorial designs a truthful CSV-to-gzip pipeline with encoding, backpressure, cancellation, integrity, worker boundaries, download sinks, and a server fallback for unsupported or unsuitable jobs.

CompressionStream starts with an export contract

Define which records, columns, ordering, locale, timezone, redaction, and snapshot boundary the file represents before choosing compression. A small gzip cannot repair an export that mixed pages from different revisions or omitted rows without disclosure.

An export starts with a data promise, not a download button. Freeze which rows, columns, filters, locale rules, and redactions belong to the edition before producing bytes. If authorization or source records change midway, a technically valid gzip file can still tell a story that never existed at one moment.

The export byte ledger logs requester, filters, snapshot, schema, ordering, row estimate, redactions, and retention in an export data manifest. That evidence lets the delivery owner reopen the original claim without guesswork. For the export byte ledger, browser compression defines what CompressionStream must prove before the delivery owner advances. Snapshot semantics determine whether the file can be trusted before compression begins. Record counts and byte estimates provide an early integrity check.

Build a byte pipeline, not a string pile

Serialize records in bounded batches, encode text through TextEncoder or an encoding transform, and enqueue Uint8Array chunks into a readable stream. Avoid concatenating the complete CSV or JSON document before compression, because that doubles peak memory at the worst moment. Read this decision through gzip stream; the export byte ledger uses it to keep CompressionStream honest for the delivery owner. Choosing a route early prevents a heroic browser pipeline from becoming the only option.

Route selection should happen before serialization allocates anything large. A small CSV may tolerate a bounded Blob, a substantial browser export needs a streaming sink, and a long-running or unsupported job belongs on the server. Device memory and sink capability matter as much as row count when choosing that path.

Attempt to export enough rows to exceed the expected mobile memory budget. The delivery owner ends the experiment when the export byte ledger shows that the full uncompressed export exists as one JavaScript string.

The byte pipeline follows Compression Streams specification, WHATWG Streams Standard, and Web Platform Tests compression suite. The Compression Streams and Streams specifications define transforms, formats, backpressure, and errors; WPT supplies interoperable behavior fixtures. Product code still owns record consistency, encoding, filenames, privacy, and delivery.

Records become bounded compressed chunksAuthorized rows enter a serializer, encoder, compression transform, paced sink, and integrity ledger without a full-memory copy.RowsBytesGzipSink
  • Rows: Freeze the snapshot
  • Bytes: Encode in batches
  • Gzip: Transform with pressure
  • Sink: Verify and close
Figure 1: Backpressure and cancellation travel through the complete byte pipeline.

Choose only a supported truthful format

Construct CompressionStream with gzip or deflate according to the recipient contract and label the filename, media type, and content encoding consistently. Do not call a gzip stream zip, brotli, or archive when it contains no archive directory or unsupported algorithm. Set a format naming table beside the outcome of inspect headers and decompress with an independent command-line tool. Any disagreement gives the export byte ledger a concrete revision target for the delivery owner.

NDJSON is useful because each row can become an independently encoded chunk with an explicit newline. A ReadableStream can pull the next batch only when downstream demand exists, keeping object lifetime short.

The serializer should also define how dates, decimals, missing fields, and embedded newlines survive round-trip inspection. Inside the export byte ledger, Web Streams API is the constraint that makes CompressionStream observable to the delivery owner. A row-oriented format makes both streaming and failure localization easier to inspect.

Let backpressure cross every stage

Use pipeThrough and pipeTo or equivalent reader-writer coordination so a slow sink limits serialization and compression work upstream. A loop that reads quickly into an ever-growing chunk array has retained the memory problem while merely changing object shapes.

A backpressure trace must carry desired size, pending writes, producer pauses, chunk sizes, compressor output, and sink latency before the delivery owner proceeds. The export byte ledger treats absent context as a broken handoff. This section lets the export byte ledger treat client-side exports as a practical test of CompressionStream for the delivery owner. Named byte stages turn memory debugging into measurement instead of guesswork.

CompressionStream works on bytes, so the pipeline boundary should be visible: records become text, text becomes encoded chunks, chunks enter gzip, and compressed output reaches a sink. Hiding those steps inside one helper makes it difficult to locate whether memory growth came from row accumulation, encoding, compression, or delivery.

Design cancellation as cleanup

Connect user cancellation, navigation, source failure, and time budgets through an AbortSignal, then close database cursors, stream controllers, workers, and partial sinks. The interface should distinguish cancelled from failed and never present a partial file as complete.

Backpressure is the browser telling the producer to slow down. A paced sink should reduce upstream pulls rather than merely queue more compressed chunks in JavaScript memory. Observe desired size, batch count, bytes in flight, and write latency under a deliberately slow destination to confirm that pressure crosses the entire chain.

Stress the premise by trying to abort during reading, encoding, compression, and final sink write. When cancel stops the progress bar but serialization continues, the export byte ledger returns the design to the delivery owner. The export byte ledger changes course when browser compression enters the CompressionStream decision owned by the delivery owner. Pressure is working only when the source performs less work, not when the queue grows elsewhere.

Pick a sink the browser can sustain

A final Blob is convenient but still materializes the compressed result in memory, which may be acceptable only under a measured cap. Larger exports need a streaming file destination where available or a server job whose completion receipt can be reopened.

After reproducing run representative exports on memory-constrained mobile and desktop cohorts, mark the divergence in a sink selection matrix. The delivery owner uses that comparison to debug the export byte ledger.

Cancellation travels in the opposite direction from data. Closing the save dialog, navigating away, or pressing cancel should stop the sink, compression transform, encoder, serializer, cursor, and database snapshot in a predictable order.

The user-facing state needs to distinguish a canceled partial file from a completed, verified export. Use gzip stream to question the export byte ledger's default assumption about CompressionStream before the delivery owner signs off. Cancellation is complete when the database cursor and user interface agree that the job ended.

OutputMemoryRecoveryRoute
Small CSVBounded BlobRestartBrowser
Large fileStream sinkPartialBrowser
Long jobServerReopenQueue
UnsupportedServerReopenFallback
Figure 2: Dataset size and sink capability choose the delivery route.

Move CPU work off the interaction path

Compression and serialization can still consume meaningful CPU even with bounded memory. Measure input delay and long tasks, then move eligible production into a worker while keeping authorization and download initiation in an explainable page-level flow. Here the export byte ledger turns Web Streams API into an explicit CompressionStream operating choice for the delivery owner. A decompression check proves the exported information survived, not merely that gzip emitted bytes.

Keep main-thread time, worker time, transfer bytes, copies, progress cadence, cancellation latency, and fallback attached to a worker boundary profile. The delivery owner can then audit the export byte ledger without reconstructing private state.

Integrity belongs after decompression, not only after a successful write call. For deterministic fixtures, decompress the produced gzip, compare the byte digest and row count with the frozen source, then parse a sample back into typed values. This catches truncation and serialization drift that a plausible file size cannot reveal.

  1. 1Rows

    Freeze the snapshot

  2. 2Bytes

    Encode in batches

  3. 3Gzip

    Transform with pressure

  4. 4Sink

    Verify and close

Figure 3: Snapshot, serialize, compress, and verify form one cancellable export.

Verify integrity after decompression

Force a run to truncate, duplicate, corrupt, and mis-encode controlled fixtures. If success means only that a download event fired, the export byte ledger has found a boundary the delivery owner must redesign. Evidence from the export byte ledger leaves CompressionStream incomplete unless client-side exports survives the delivery owner's case. Fallback parity keeps the export's meaning stable across browsers and devices.

Count records and uncompressed bytes during production, hash the canonical byte stream where appropriate, and test independent decompression before declaring success. The compressed size alone cannot reveal truncation, duplicate batches, encoding loss, or a sink that closed early.

Browser support and sink behavior need a tested fallback ladder. Detect APIs without trusting a user-agent string, preserve the same export contract on the server route, and explain why the path changed. A fallback that drops columns, filters, or cancellation semantics is a different feature wearing the same button label.

Export delivery intersects with export buttons need data contracts, streaming NDJSON parser, cancellable fetch pipelines, and OffscreenCanvas generative posters. Export contracts, streaming parsing, abort composition, and off-thread creative work contribute adjacent patterns. The compressor should receive already-authorized bytes and expose cancellation without becoming the owner of data selection.

Release with privacy and fallback evidence

Document sensitive fields, local residue, object-URL revocation, browser support, server fallback, progress copy, and the threshold that changes routes. Keep the WPT-inspired format corpus and a production-shaped memory trace in the release packet.

At release time, browser compression is how the export byte ledger lets the delivery owner verify CompressionStream. Receipts should explain the edition while revealing as little of its content as possible. An export receipt should make support work concrete: snapshot identifier, policy version, route, row count, uncompressed and compressed bytes, timings, cancellation state, digest, filename, and browser capability. That record lets a user report one failed edition without attaching the sensitive file itself to a ticket.

Release evidence pairs a browser export release receipt with a run that can force feature detection off and complete the same authorized export server-side. The delivery owner withholds the export byte ledger decision until both accounts agree.

The policy fixture keeps large supported gzip work in a streaming browser lane and routes unsupported formats server-side.

Runnable artifact — browser-export-policy.test.mjs

import assert from "node:assert/strict";import {createHash} from "node:crypto";
const rows=Array.from({length:128},(_,id)=>JSON.stringify({id,label:"row-"+id,value:id*7})).join("\n")+"\n",bytes=new TextEncoder().encode(rows),digest=x=>createHash("sha256").update(x).digest("hex");
const source=new ReadableStream({start(c){for(let i=0;i<bytes.length;i+=97)c.enqueue(bytes.slice(i,i+97));c.close()}});
const compressed=new Uint8Array(await new Response(source.pipeThrough(new CompressionStream("gzip"))).arrayBuffer());assert.ok(compressed.length<bytes.length);
const restored=new Uint8Array(await new Response(new Blob([compressed]).stream().pipeThrough(new DecompressionStream("gzip"))).arrayBuffer());assert.equal(digest(restored),digest(bytes));assert.equal(new TextDecoder().decode(restored),rows);
let cancelled="";const endless=new ReadableStream({pull(c){c.enqueue(bytes.slice(0,64))},cancel(reason){cancelled=reason}});const reader=endless.pipeThrough(new CompressionStream("gzip")).getReader();await reader.read();await reader.cancel("user-stop");await new Promise(r=>setTimeout(r,0));assert.equal(cancelled,"user-stop");
console.log("PASS: export policy stays stream-bounded");

Run node browser-export-policy.test.mjs. Expected receipt: PASS: export policy stays stream-bounded.

Use CompressionStream when the browser can produce bytes incrementally and the supported format matches the recipient's needs. Move exports to a server when data authority, duration, browser sinks, recovery, or resource budgets exceed a page's reliable ownership.