@portable/lines
Splits a stream of bytes or text into lines. Runs in browsers, Cloudflare Workers, Deno, Bun and Node 24+.
npm install @portable/linesimport * as Lines from '@portable/lines';
import * as Streams from '@portable/streams';
const response = await fetch('https://example.com/access.log');
const lines = response.body!.pipeThrough(new Lines.DecoderStream());
for await (const line of Streams.toAsyncIterable(lines)) {
console.log(line);
}Lines.DecoderStream is a TransformStream<Uint8Array | string, string>. Byte chunks are decoded with TextDecoder, so a multi-byte character split across chunks is decoded correctly, and a CRLF split across chunks counts as one line break.
Options
| Option | Default | Description |
|---|---|---|
separator | /\r?\n/ | A string, a RegExp, or any object with [Symbol.split]. With the default or a string, a line that spans many chunks is split in linear time. Any other separator splits the unfinished line again for each chunk, so set maxLength when lines can be long. |
encoding | 'utf-8' | TextDecoder label for byte chunks. |
maxLength | Infinity | Longest allowed line, in UTF-16 code units. |
overflow | 'error' | When a line exceeds maxLength: 'error' fails the stream with MaxLengthError, 'skip' drops the line. |
skipEmpty | false | Drop empty lines. |
signal | none | Aborting errors the stream with signal.reason and cancels the source piped into it. |
Types
| Type | Description |
|---|---|
Lines.Separator | The separator option: a string to match, or anything with [Symbol.split], such as a RegExp. |
Why use this over split2 and byline
split2 and byline are Node Transform streams. Lines.DecoderStream is a TransformStream, so it splits a fetch response body in any JavaScript runtime, and it has no dependencies outside @portable/*.
- It split 1.7 MB of text at the same speed as split2 in Node.
node:readlinewas about 2× faster than both, but it exists only in Node. maxLengthis checked on every line. split2 checks only the unfinished line at the end of each chunk, so whether it catches a long line depends on how the input was chunked.- A multi-byte character or a CRLF split across two chunks is read as one character or one line break.
- Empty lines are kept by default, as in split2. byline drops them unless you pass
keepEmptyLines, andskipEmpty: truedrops them here. - split2 takes a mapper function, such as
split2(JSON.parse).Lines.DecoderStreamhas no mapper, so pipe its output through anotherTransformStream. - byline splits on every Unicode line break by default. Pass
separator: /\r\n|[\n\v\f\r\x85\u2028\u2029]/for the same result.
| @portable/lines | split2 | byline | node:readline | |
|---|---|---|---|---|
| Split 1.7 MB into lines | 82.8 | 84.6 | 40.7 | 165 |
Operations per second from pnpm bench, higher is better, in Node 24.22.0, on Intel(R) Xeon(R) Processor @ 2.10GHz (4 cores). A dash means the library has no equivalent.
| @portable/lines | split2 | byline | node:readline | |
|---|---|---|---|---|
| Minified and gzipped | 1.3 kB | 1.1 kB | 1.2 kB | built in |
| Runtime dependencies | 3 | 0 | 0 | – |
| Node built-ins used | none | stream, string_decoder | Buffer, stream, timers, util | Node only |
| TypeScript types | included | @types/split2 | @types/byline | – |
| Version compared | – | 4.2.0 | 5.0.0 | – |
Sizes cover the imports in the usage example, or the equivalent imports of each library, bundled for the browser by scripts/bundle-size.ts. Runtime dependencies count every package installed with the published package. A library that uses Node built-ins needs polyfills in a browser and the nodejs_compat flag in Cloudflare Workers.