pbf: protocol buffers for JavaScript at 2.5 KB gzipped
A low-level, lightweight protocol buffers implementation in JavaScript.
At a glance
- What is it?
- Mapbox's pbf is a hand-written protocol buffers implementation for JavaScript that trades generated code for a small bundle, shipping a reader, a writer and a schema compiler so you can decode and encode binary data by hand. The trade is that you write the field handling yourself instead of getting it generated.
- Who is it for?
- Use pbf when the bytes matter more than the ergonomics, which in practice means browser bundles where a few kilobytes decide whether you ship a feature, and formats like vector tiles where you want to skip fields you do not need. Do not reach for it when you want a generated message class and reflection, because writing the field dispatch by hand is the price you pay for the size.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 51 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two classes, split so bundlers can drop one
The library exposes exactly two classes: PbfReader for decoding and PbfWriter for encoding. That split is deliberate, and the reason given is that it lets bundlers tree-shake the half you do not use. A web application that only reads tiles should not ship the writer. The package ships as an ES module, with the main entry at index.js and a second export at ./compile for the schema compiler, and the type declaration file index.d.ts is published alongside. The whole package has one runtime dependency, resolve-protobuf-schema, and everything else in the package file is a development dependency for linting, testing, benchmarking and building. A library that small with a dependency list that short is unusual in the protobuf space, and it is the direct consequence of not shipping a code generator and a runtime schema engine.
Decoding is a field loop you write yourself
There is no generated message class and no reflection. You loop over the fields of a message with nextField and dispatch on the field number, and unrecognized fields are skipped automatically on the next iteration.
let field;
while ((field = pbf.nextField(end))) {
if (field === 1) obj.id = pbf.readVarint();
else if (field === 2) obj.name = pbf.readString();
}The pattern is a dispatch chain of if and else statements, one per field number, and the reader methods are named after the wire types they consume. There are readVarint with a signed flag, readSVarint, readFixed32, readFixed64, readSFixed32, readSFixed64, readBoolean, readFloat, readDouble, readString and readBytes, plus the packed variants such as readPackedVarint and readPackedDouble which append into an array you pass in. To read an embedded message you pass the length plus the current position as the end boundary to a nested reader, which is how a nested message knows where it stops.
const msg = readSubMessage(pbf, pbf.readVarint() + pbf.pos);A callback-based readFields(fn, obj, end) is still available for backward compatibility, but the README is direct about it: new code should prefer the nextField loop because it is significantly faster.
Lazy decoding lets you skip what you do not need
The feature list calls out lazy decoding, and the mechanism is a saved position. You record where a field began, keep scanning, and come back to it later if you decide you want the value.
let fooPos = -1;
let field;
while ((field = pbf.nextField())) {
if (field === 1) fooPos = pbf.pos;
}
...
pbf.pos = fooPos;
const foo = readFoo(pbf, pbf.readVarint() + pbf.pos);The reader exposes length, the length of the underlying buffer, and pos, the current offset for reading or writing, and pos is writable, which is what makes the jump possible. Both classes expose those two properties. This is the feature that matters most for large binary payloads, since a full decode allocates an object graph for every field whether or not you read it, while a lazy read touches only the bytes you actually need. It is also the reason the library is a good fit for vector tiles, where a layer may contain fields for geometry, text and attributes and a client typically wants two of the three.
The size argument in the README's benchmark table
The performance section makes two claims. The library is fast, competitive with or faster than other JavaScript protobuf implementations, while being an order of magnitude smaller. The table it prints measures decoding and encoding 439 Mapbox vector tiles, 37.5 MB in total, with the equivalent JSON at 136 MB, on Node v26. Against protocol-buffers the library decodes in 195ms at 192 MB per second and encodes in 146ms at 257 MB per second, while protocol-buffers takes 303ms and 612ms, with bundle sizes of 2.6 KB against 14.2 KB. protobuf.js is listed at 192ms decode and 141ms encode with a 33.9 KB bundle. The JSON row is measured against the 136 MB JSON payload rather than the 37.5 MB binary one, and on the same data the README says pbf is about twice as fast to decode and two and a half times as fast to encode, producing output roughly a quarter the size. The benchmark script is bench/bench-tiles.js. Two caveats are worth holding on to. These are the maintainers' own numbers on vector tiles, and the size column is minzip, the smallest package, not what your bundler will emit.
Compiling a schema without a code generator
The package ships a bin entry named pbf and a compile.js export, so there is a command line path as well as a runtime one. Install the package globally and point it at a .proto file, and it writes a JavaScript module.
$ npm install -g pbf
$ pbf example.proto > example.jsThe generated module exports read and write functions that take a reader or a writer, and you use them like this.
import {PbfReader, PbfWriter} from 'pbf';
import {readExample, writeExample} from './example.js';
// read
const obj = readExample(new PbfReader(buffer));
// write
const pbf = new PbfWriter();
writeExample(obj, pbf);
const buffer = pbf.finish();There is also a route that parses a schema at runtime instead of at build time, using the compile function from pbf/compile together with a parsed schema.
import {compile} from 'pbf/compile';
import schema from 'protocol-buffers-schema';
const proto = schema.parse(fs.readFileSync('example.proto'));
const {readExample, writeExample} = compile(proto);So the choice is between generating a module once and shipping it, or parsing the schema in the browser at startup, which is more flexible and slower.
Writing mirrors the reading side
The writer mirrors the reader, with a per-field method for each wire type: writeVarintField, writeSVarintField, writeFixed32Field, writeFixed64Field, writeSFixed32Field, writeSFixed64Field, writeBooleanField and the rest, plus the plain value writers such as writeVarint and writeString. A writer can be created with no arguments or with a pre-allocated Buffer or Uint8Array, which is the difference between growing a buffer as you write and telling the writer up front how much space it needs. Embedded messages are written with writeMessage, taking a tag, a write function and the object.
pbf.writeMessage(1, writeObj, obj);
function writeObj(obj, pbf) {
pbf.writeStringField(obj.name);
pbf.writeVarintField(obj.version);
}The buffer is finalised with finish(), which is the call that produces the bytes. The hand-written alternative, without any generated module, is the same shape: a write function per message that calls the field methods in order.
Where this library is the wrong choice
The trade-off is explicit and worth restating before you commit. pbf gives you a small bundle and a fast reader at the cost of writing your own field dispatch, which means every message type in your project is code you maintain, and a schema change means editing that code. If you want generated classes, a descriptor-based runtime or the safety of a schema-driven encoder, one of the other JavaScript protobuf implementations is the better fit and the size difference will not matter to you. A second constraint is the ES module only packaging. The package is type module with an exports map, and the browser usage documented is either an ES module import from jsDelivr or a browser bundle that exposes a Pbf global with PbfReader and PbfWriter properties. There is no CommonJS entry, which will matter if you maintain code that still requires modules. The package also passes an allowScripts entry for fsevents, a macOS file watching dependency, and it ships a pbf binary in the bin directory alongside the library.
Release cadence and the licence
The project is actively released. Version v5.1.2 was published on 2026-07-09, v5.1.1 on 2026-07-08 and v5.1.0 on 2026-05-29, and the last push to the repository was on 2026-09-02. Three releases in one patch series in 2026 with commits still landing afterwards suggests a maintained line rather than a finished one. The build tooling is modern for a library of this age: Rollup for the bundle with the terser plugin, ESLint 10 with the mourner config, TypeScript 7 for the type check in the pretest script, and the test runner is the Node built-in test runner invoked as node --test test/*.test.js, with a coverage variant using the experimental coverage flag. The licence field on the repository says BSD-3-Clause, and the author recorded in the package metadata is Konstantin Kaefer. The repository also carries a CODEOWNERS file and a tsconfig.json.
Editorial conclusion
Use pbf when the bytes matter more than the ergonomics, which in practice means browser bundles where a few kilobytes decide whether you ship a feature, and formats like vector tiles where you want to skip fields you do not need. Do not reach for it when you want a generated message class and reflection, because writing the field dispatch by hand is the price you pay for the size. Verify first that your toolchain tolerates the deliberate lack of codegen, that you can live with the nextField loop rather than the older readFields callback, and that the bundle size you measure after bundling matches the 2.5 KB gzipped figure the README quotes, since that number is for the library alone and not for your application.
Frequently asked questions
How do I install pbf and use it in Node?
Install with npm install pbf and import the two classes as a module: import {PbfReader, PbfWriter} from 'pbf'. The package main entry is index.js and it is published as an ES module, with the package type set to module.
How do I decode a protobuf message with pbf?
Loop over fields with nextField and dispatch on the field number, reading values with methods such as readVarint, readString and readBytes. Unrecognized fields are skipped automatically on the next iteration, and the callback style readFields is kept only for backward compatibility because the nextField loop is significantly faster.
What is the bundle size of pbf compared to other protobuf libraries?
The README describes it as ultra-lightweight at 2.5 KB gzipped, and its benchmark table lists a minzip size of 2.6 KB against 14.2 KB for protocol-buffers and 33.9 KB for protobuf.js, measured on 439 Mapbox vector tiles totalling 37.5 MB.
Can I compile a .proto file into JavaScript with pbf?
Yes. The package ships a bin entry, so installing it globally and running pbf example.proto > example.js writes a module exporting read and write functions. You can also parse a schema at runtime with the compile function from pbf/compile.
Does pbf work in the browser?
Yes. It works in both Node and the browser. The README documents an ES module import from jsDelivr and a browser bundle that exposes a Pbf global with PbfReader and PbfWriter properties.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mapbox-pbf)Was this useful?