Skip to content
LogoLogo

@portable/base64

Encodes and decodes base64 and base64url, with optional line wrapping for MIME and streams for large bodies. Runs in browsers, Cloudflare Workers, Deno, Bun and Node 24+.

Replaces libbase64, base64-js and js-base64.

npm install @portable/base64
import * as Base64 from '@portable/base64';
 
Base64.encode('hello'); // 'aGVsbG8='
Base64.encode(bytes, { alphabet: 'base64url', omitPadding: true });
Base64.encode(attachment, { lineLength: 76 }); // MIME body, CRLF between lines
 
new TextDecoder().decode(Base64.decode('aGVsbG8=')); // 'hello'
Base64.decode(mimeBody, { mode: 'mime' });
 
const response = await fetch('https://example.com/logo.png');
const body = response.body!.pipeThrough(
  new Base64.EncoderStream({ lineLength: 76 }),
);

encode and decode call Uint8Array.prototype.toBase64 and Uint8Array.fromBase64 where the runtime has them (current Chromium, Firefox and Safari). Elsewhere, Node 24 included, a JavaScript fallback gives the same output and throws on the same input. The browser tests check the two against each other.

API

ExportDoes
encode(input, options?)Encodes a Uint8Array, or a string as UTF-8, to a base64 string.
decode(input, options?)Decodes a base64 string, or its bytes, to a Uint8Array.
new EncoderStream(options?)TransformStream<Uint8Array | string, Uint8Array> that emits the base64 text as ASCII bytes. However the input is split into chunks, the output equals encode of the whole input.
new DecoderStream(options?)TransformStream<string | Uint8Array, Uint8Array>. Same output as decode of the whole input. Line breaks, padding and groups of four characters can be split across chunks anywhere.
InvalidInputErrorThrown, or errors the stream, for a character outside the alphabet or misplaced padding.
TruncatedErrorThrown, or errors the stream, when the input ends inside a group of four characters.

Encode options

encode and EncoderStream take the same options.

OptionDefaultDescription
alphabet'base64''base64' uses + and /. 'base64url' uses - and _ (RFC 4648 §5).
omitPaddingfalseLeave out the trailing =.
lineLengthfalseCharacters per line, or false for one line. MIME uses 76. The output never ends with a line ending.
newline'\r\n'Inserted between lines.
signalnoneEncoderStream only. Aborting errors the stream with signal.reason and cancels the source piped into it.

Decode options

decode and DecoderStream take the same options.

OptionDefaultDescription
alphabet'base64'Alphabet of the input. 'mime' mode accepts both.
mode'loose'How to treat input that isn't canonical base64, below.
signalnoneDecoderStream only. Aborting errors the stream with signal.reason and cancels the source piped into it.
ModeWhitespaceMissing paddingOther charactersData after padding
'loose'skippedacceptedInvalidInputErrorInvalidInputError
'strict'skippedTruncatedErrorInvalidInputErrorInvalidInputError
'mime'skippedacceptedskippeddecoded as a new segment

'loose' and 'strict' match Uint8Array.fromBase64 with lastChunkHandling set to the same name. 'strict' also rejects non-zero bits before the padding ('YR=='). Both throw TruncatedError for one leftover character ('YWJjZ') or a single = where two are needed ('YQ=').

'mime' follows RFC 2045 §6.8, which tells decoders to ignore characters outside the alphabet, and libbase64, which decodes each =-terminated segment on its own. It reads both alphabets and never throws, so use it for mail bodies and other input you don't control.

Types

TypeDescription
Base64.Alphabet'base64' or 'base64url' (RFC 4648).
Base64.Mode'loose', 'strict' or 'mime', how decode treats input that isn't canonical base64.

Why use this over base64-js, js-base64 and libbase64

One package does what the three did separately: base64 and base64url, MIME line wrapping, and streams. It calls the platform's toBase64 and fromBase64 where the runtime has them, and depends only on other @portable/* packages.

  • In Node, which has no native base64 on Uint8Array, encoding 1 MB was about 8× faster than base64-js and 3× faster than js-base64. Decoding 1 MB was as fast as base64-js and about 11× faster than js-base64.
  • EncoderStream at 76 columns was as fast as libbase64's Encoder, and DecoderStream was about 1.3× faster than its Decoder.
  • Buffer is 11 to 21× faster on 1 MB in Node, and so are libbase64's plain encode and decode, which call Buffer. If your code runs only in Node and you need that speed, keep using Buffer.
  • The default mode throws InvalidInputError on characters outside the alphabet. base64-js drops them and returns wrong bytes (base64-js #81), and throws a RangeError on '====' (base64-js #73).
  • encode throws a TypeError for an Int8Array, which base64-js encoded to wrong output (base64-js #38), and the stream output doesn't depend on chunk boundaries, which libbase64 fixed only in 1.3.2.

Performance

Node 24 has no native base64 on Uint8Array, so it runs the fallback. libbase64's encode and decode call Buffer, which is C++.

@portable/base64base64-jsjs-base64libbase64Buffer (Node)
Encode 1 MB43555.11485,0004,740
Encode 1 MB in 76-column lines435––554–
Decode 1 MB25523922.44,2105,450
Decode 1 MB of MIME base64219––3,900–
Encode 32 bytes3,450,0001,050,0002,420,000–2,370,000
Decode 32 bytes3,110,0002,750,0001,130,000–2,360,000
Encode a 1 MB stream in 76-column lines413––394–
Decode a 1 MB stream of MIME base64233––183–

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/base64base64-jsjs-base64libbase64Buffer (Node)
Minified and gzipped3.3 kB1.1 kB1.6 kB1.8 kBbuilt in
Runtime dependencies3000–
Node built-ins usednonenonenonebuffer, streamNode only
TypeScript typesincludedincludedincludednone–
Version compared–1.5.13.9.41.3.3–

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.

Differences to check when you switch:

  • libbase64's Encoder wraps lines at 76 characters by default, and encode and EncoderStream write one line unless you pass lineLength: 76.
  • libbase64 rounds a fractional line length down and replaces any other invalid value with 76. lineLength must be a positive integer or false, and anything else throws RangeError.
  • libbase64 and js-base64 skip characters outside the alphabet and data after padding, and base64-js stops at the first =. The default 'loose' mode throws InvalidInputError for both, and 'mime' mode gives libbase64's result.
  • base64-js and js-base64 accept +/ and -_ mixed in one string. decode accepts only the alphabet that alphabet selects, except in 'mime' mode.
  • js-base64's decode returns a string, and decode returns bytes, so pass them to TextDecoder to get the text.
  • libbase64's decode returns a Buffer argument unchanged. decode reads a Uint8Array as base64 text.
  • js-base64 encodes lone surrogates as they are. encode encodes strings with TextEncoder, which replaces a lone surrogate with U+FFFD.
  • libbase64's Encoder options skipStartBytes, limitOutputBytes and startPadding and its byte counters have no equivalent in EncoderStream, and string chunks are always encoded as UTF-8.