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
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.
- 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().
- 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.
- 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.
After #1760, and once #1763 and #1764 land,
import stream from 'stream'inlib/document.jsis 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
Readableactually buys us_read()is a no-op and_writeignores the return value ofpush(), so backpressure never engages on the read side (#269). The document is built in memory before anything consumes it anyway. What consumers use ison('data'),on('end'),pipe()and occasionally async iteration.pipe()is the only piece with real logic, since it has to honourdrainfrom 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
streamtovite-compatible-readable-streamand runsrollup-plugin-polyfill-nodeon top, which drags inevents,inherits,string_decoder,util-deprecateand aprocess.nextTickshim. Replacing all of that with a ~120 line class implementing the four things above:Measured with
esbuild --minifyon the rollup output, no other change in the tree.Options
stream.Readablein Node, a minimal class in the browser, via the samenode/defaultcondition split the build already uses for#zlib. Node consumers keep the fullReadablesurface:pipeline(),destroy(), backpressure, all of it. Browser consumers geton/once/off/emit/push/pipe/Symbol.asyncIteratorand nothing else. Cost is two behaviours to document and test.stream.pipeline(doc, res),setEncoding(),readable/error/closeevents,.read().ReadableStreamin the browser. No dependency and a real platform primitive, but it does not speakon('data')orpipe(), so every browser snippet in the docs breaks, including the blob-stream one.readable-streama 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 astream.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.