Skip to content

Node's stream is the last builtin left in the browser build #1765

Description

@diegomura

After #1760, and once #1763 and #1764 land, import stream from 'stream' in lib/document.js is the only Node builtin the browser bundle still has to resolve. Bundlers stopped polyfilling core modules by default, so a browser consumer either configures a shim or the build breaks. I would like to close that out, but the answer is less obvious than the previous three, so opening this to agree on a direction before writing code.

What Readable actually buys us

_read() is a no-op and _write ignores the return value of push(), so backpressure never engages on the read side (#269). The document is built in memory before anything consumes it anyway. What consumers use is on('data'), on('end'), pipe() and occasionally async iteration. pipe() is the only piece with real logic, since it has to honour drain from the destination (fs.createWriteStream, an HTTP response).

What it costs

react-pdf's fork is close enough to upstream now to measure on. Its browser build aliases stream to vite-compatible-readable-stream and runs rollup-plugin-polyfill-node on top, which drags in events, inherits, string_decoder, util-deprecate and a process.nextTick shim. Replacing all of that with a ~120 line class implementing the four things above:

before after
browser bundle, minified 198.0 KB 123.2 KB
minified + gzip 64.6 KB 42.2 KB

Measured with esbuild --minify on the rollup output, no other change in the tree.

Options

  1. stream.Readable in Node, a minimal class in the browser, via the same node/default condition split the build already uses for #zlib. Node consumers keep the full Readable surface: pipeline(), destroy(), backpressure, all of it. Browser consumers get on/once/off/emit/push/pipe/Symbol.asyncIterator and nothing else. Cost is two behaviours to document and test.
  2. The minimal class everywhere. One code path, but breaking for Node users who treat the document as a real stream: stream.pipeline(doc, res), setEncoding(), readable/error/close events, .read().
  3. WHATWG ReadableStream in the browser. No dependency and a real platform primitive, but it does not speak on('data') or pipe(), so every browser snippet in the docs breaks, including the blob-stream one.
  4. Make readable-stream a real dependency. Smallest change and no polyfill config for users, but it keeps ~75 KB minified of Node stream machinery in the browser for a class that never uses backpressure.

I lean towards 1.

The blob-stream angle

Worth flagging since it overlaps. The documented browser recipe is doc.pipe(blobStream()), and blob-stream is itself a stream.Writable, so a browser user pulls the shim back in through the side door even after pdfkit drops it. #1345 asks for the same thing from the other end. If the minimal class lands, shipping a small blob sink next to it (toBlob() / toBlobURL()) would make the browser path dependency free end to end. Happy to keep that as a separate change if you prefer.

Which direction do you want? I have 1 working in the react-pdf fork and can port it here.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions