Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2024, Mapbox
Copyright (c) 2026, Mapbox
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down
65 changes: 38 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ A low-level, fast, ultra-lightweight (3KB gzipped) JavaScript library for decodi

## Performance

This library is extremely fast — much faster than native `JSON.parse`/`JSON.stringify`
and the [protocol-buffers](https://github.com/mafintosh/protocol-buffers) module.
Here's a result from running a real-world benchmark on Node v6.5
(decoding and encoding a sample of 439 vector tiles, 22.6 MB total):

- **pbf** decode: 387ms, or 57 MB/s
- **pbf** encode: 396ms, or 56 MB/s
- **protocol-buffers** decode: 837ms, or 26 MB/s
- **protocol-buffers** encode: 4197ms, or 5 MB/s
- **JSON.parse**: 1540ms, or 15 MB/s (parsing an equivalent 77.5 MB JSON file)
- **JSON.stringify**: 607ms, or 49 MB/s
This library is fast — competitive with or faster than other JS protobuf implementations,
and orders of magnitude smaller. Here's a result from a real-world benchmark on Node v26
(decoding and encoding 439 Mapbox vector tiles, 37.5 MB total; the equivalent JSON is 136 MB):

| | decode | encode |
|------------------|-------------------|-------------------|
| **pbf** | 200ms, 187 MB/s | 188ms, 200 MB/s |
| protocol-buffers | 297ms, 126 MB/s | 620ms, 60 MB/s |
| protobuf.js | 226ms, 169 MB/s | 510ms, 74 MB/s |
| JSON | 488ms, 278 MB/s | 267ms, 509 MB/s |

`JSON` throughput is measured against the 136 MB JSON payload, not the 37.5 MB pbf payload —
on the same data, pbf is ~2× faster to decode and ~2.5× faster to encode, and produces output
roughly a quarter the size. See `bench/bench-tiles.js`.

## Examples

Expand All @@ -33,14 +36,14 @@ $ pbf example.proto > example.js
Then read and write objects using the module like this:

```js
import Pbf from 'pbf';
import {PbfReader, PbfWriter} from 'pbf';
import {readExample, writeExample} from './example.js';

// read
var obj = readExample(new Pbf(buffer));
const obj = readExample(new PbfReader(buffer));

// write
const pbf = new Pbf();
const pbf = new PbfWriter();
writeExample(obj, pbf);
const buffer = pbf.finish();
```
Expand All @@ -58,7 +61,7 @@ const {readExample, writeExample} = compile(proto);
#### Custom Reading

```js
var data = new Pbf(buffer).readFields(readData, {});
const data = new PbfReader(buffer).readFields(readData, {});

function readData(tag, data, pbf) {
if (tag === 1) data.name = pbf.readString();
Expand All @@ -74,9 +77,9 @@ function readLayer(tag, layer, pbf) {
#### Custom Writing

```js
var pbf = new Pbf();
const pbf = new PbfWriter();
writeData(data, pbf);
var buffer = pbf.finish();
const buffer = pbf.finish();

function writeData(data, pbf) {
pbf.writeStringField(1, data.name);
Expand All @@ -94,36 +97,38 @@ function writeLayer(layer, pbf) {
Install using NPM with `npm install pbf`, then import as a module:

```js
import Pbf from 'pbf';
import {PbfReader, PbfWriter} from 'pbf';
```

Or use as a module directly in the browser with [jsDelivr](https://www.jsdelivr.com/esm):

```html
<script type="module">
import Pbf from 'https://cdn.jsdelivr.net/npm/pbf/+esm';
import {PbfReader, PbfWriter} from 'https://cdn.jsdelivr.net/npm/pbf/+esm';
</script>
```

Alternatively, there's a browser bundle with a `Pbf` global variable:
Alternatively, there's a browser bundle exposing a `Pbf` global with `PbfReader` and `PbfWriter` properties:

```html
<script src="https://cdn.jsdelivr.net/npm/pbf"></script>
```

## API

Create a `Pbf` object, optionally given a `Buffer` or `Uint8Array` as input data:
The library exposes two classes: `PbfReader` for decoding and `PbfWriter` for encoding. Splitting them lets bundlers tree-shake the half you don't use.

Create a `PbfReader` from a `Buffer` or `Uint8Array`:

```js
// parse a pbf file from disk in Node
const pbf = new Pbf(fs.readFileSync('data.pbf'));
const pbf = new PbfReader(fs.readFileSync('data.pbf'));

// parse a pbf file in a browser after an ajax request with responseType="arraybuffer"
const pbf = new Pbf(new Uint8Array(xhr.response));
const pbf = new PbfReader(new Uint8Array(xhr.response));
```

`Pbf` object properties:
Both classes expose the following properties:

```js
pbf.length; // length of the underlying buffer
Expand All @@ -143,7 +148,7 @@ pbf.readFields((tag) => {
```

It optionally accepts an object that will be passed to the reading function for easier construction of decoded data,
and also passes the `Pbf` object as a third argument:
and also passes the `PbfReader` object as a third argument:

```js
const result = pbf.readFields(readField, {})
Expand Down Expand Up @@ -205,6 +210,12 @@ Packed reading methods:

#### Writing

Create a `PbfWriter` (optionally with a pre-allocated `Buffer` or `Uint8Array`):

```js
const pbf = new PbfWriter();
```

Write values:

```js
Expand Down Expand Up @@ -287,7 +298,7 @@ The `--legacy` switch makes it generate a CommonJS module instead of ESM.
`Pbf` will generate `read<Identifier>` and `write<Identifier>` functions for every message in the schema. For nested messages, their names will be concatenated — e.g. `Message` inside `Test` will produce `readTestMessage` and `writeTestMessage` functions.


* `read(pbf)` - decodes an object from the given `Pbf` instance.
* `write(obj, pbf)` - encodes an object into the given `Pbf` instance (usually empty).
* `read(pbf)` - decodes an object from the given `PbfReader` instance.
* `write(obj, pbf)` - encodes an object into the given `PbfWriter` instance (usually empty).

The resulting code is clean and simple, so it's meant to be customized.
98 changes: 75 additions & 23 deletions bench/bench-tiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,44 @@ import {createHash} from 'crypto';
import {mkdirSync, existsSync, readFileSync, writeFileSync} from 'fs';
import {join, dirname} from 'path';
import {fileURLToPath} from 'url';
import protocolBuffers from 'protocol-buffers';
import protobufjs from 'protobufjs';
import {readTile, writeTile} from '../test/fixtures/vector_tile.js';
import Pbf from '../index.js';
import {PbfReader, PbfWriter} from '../index.js';

const token = process.env.ACCESS_TOKEN;
if (!token) throw new Error('Missing ACCESS_TOKEN environment variable (Mapbox access token).');
const vtProtoUrl = new URL('../test/fixtures/vector_tile.proto', import.meta.url);
const ProtocolBuffersTile = protocolBuffers(readFileSync(vtProtoUrl)).Tile;
const ProtobufjsTile = protobufjs.loadSync(fileURLToPath(vtProtoUrl)).lookup('vector_tile.Tile');

const tilesetId = 'mapbox.mapbox-streets-v8';
const url = `https://api.mapbox.com/v4/${tilesetId}/{z}/{x}/{y}.mvt?access_token=${token}`;
const urlTemplate = `https://api.mapbox.com/v4/${tilesetId}/{z}/{x}/{y}.mvt?access_token={token}`;

const libs = {
pbf: {
decode: body => readTile(new PbfReader(body)),
encode: (tile) => { const pbf = new PbfWriter(); writeTile(tile, pbf); return pbf.finish(); }
},
'protocol-buffers': {
decode: body => ProtocolBuffersTile.decode(body),
encode: tile => ProtocolBuffersTile.encode(tile)
},
'protobuf.js': {
decode: body => ProtobufjsTile.decode(body),
encode: tile => ProtobufjsTile.encode(tile).finish()
},
JSON: {
decode: body => JSON.parse(body),
encode: tile => JSON.stringify(tile)
}
};

let readTime = 0;
let writeTime = 0;
const stats = {};
for (const name of Object.keys(libs)) stats[name] = {read: 0, write: 0};
let size = 0;
let jsonSize = 0;
let numTiles = 0;

await runStats(url, processTile, showStats, {
await runStats(urlTemplate, processTile, showStats, {
width: 2880,
height: 1800,
minZoom: 0,
Expand All @@ -30,23 +53,47 @@ function processTile(body) {
size += body.length;
numTiles++;

let now = clock();
const tile = readTile(new Pbf(body));
readTime += clock(now);

now = clock();
const pbf = new Pbf();
writeTile(tile, pbf);
const buf = pbf.finish();
writeTime += clock(now);

console.assert(buf);
// pre-decode once with pbf to get a tile object usable as input for encoders
const tile = readTile(new PbfReader(body));
const tileJSON = JSON.stringify(tile);
jsonSize += tileJSON.length;
const pbInput = ProtocolBuffersTile.decode(body);
const pbjsInput = ProtobufjsTile.decode(body);

const inputs = {
pbf: body,
'protocol-buffers': body,
'protobuf.js': body,
JSON: tileJSON
};
const encodeInputs = {
pbf: tile,
'protocol-buffers': pbInput,
'protobuf.js': pbjsInput,
JSON: tile
};

for (const name of Object.keys(libs)) {
let now = clock();
libs[name].decode(inputs[name]);
stats[name].read += clock(now);

now = clock();
libs[name].encode(encodeInputs[name]);
stats[name].write += clock(now);
}
}

function showStats() {
console.log('%d tiles, %d KB total', numTiles, Math.round(size / 1024));
console.log('read time: %dms, or %d MB/s', Math.round(readTime), speed(readTime, size));
console.log('write time: %dms, or %d MB/s', Math.round(writeTime), speed(writeTime, size));
console.log('%d tiles, %d KB pbf / %d KB JSON', numTiles, Math.round(size / 1024), Math.round(jsonSize / 1024));
for (const name of Object.keys(libs)) {
const inSize = name === 'JSON' ? jsonSize : size;
const s = stats[name];
console.log('%s decode: %dms, %d MB/s encode: %dms, %d MB/s',
name.padEnd(18),
Math.round(s.read), speed(s.read, inSize),
Math.round(s.write), speed(s.write, inSize));
}
}

function speed(time, size) {
Expand All @@ -61,7 +108,8 @@ function clock(start) {

async function runStats(urlTemplate, onTile, onDone, options) {
const cacheRoot = join(dirname(fileURLToPath(import.meta.url)), 'tile-cache');
const cachePath = join(cacheRoot, createHash('sha1').update(urlTemplate).digest('hex').slice(0, 8));
const cacheKey = urlTemplate.replace(/access_token=[^&]*/, '');
const cachePath = join(cacheRoot, createHash('sha1').update(cacheKey).digest('hex').slice(0, 8));
mkdirSync(cachePath, {recursive: true});

const tilePromises = [];
Expand Down Expand Up @@ -97,10 +145,14 @@ async function loadTile(z, x, y, urlTemplate, cachePath) {
return readFileSync(tilePath);
}

const token = process.env.MAPBOX_ACCESS_TOKEN;
if (!token) throw new Error('Missing MAPBOX_ACCESS_TOKEN environment variable.');

const tileUrl = urlTemplate
.replace('{x}', x)
.replace('{y}', y)
.replace('{z}', z);
.replace('{z}', z)
.replace('{token}', token);

const response = await fetch(tileUrl);

Expand Down
8 changes: 4 additions & 4 deletions bench/bench.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
</head>
<body>
<script type="module">
import Pbf from '../index.js';
import {readTile, writeTile} from './vector_tile.js';
import {PbfReader, PbfWriter} from '../index.js';
import {readTile, writeTile} from '../test/fixtures/vector_tile.js';

function read(data) {
return readTile(new Pbf(data));
return readTile(new PbfReader(data));
}
function write(tile) {
var pbf = new Pbf();
const pbf = new PbfWriter();
writeTile(tile, pbf);
return pbf.finish();
}
Expand Down
28 changes: 14 additions & 14 deletions bench/bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,47 @@ import protocolBuffers from 'protocol-buffers';
import protobufjs from 'protobufjs';

import {readTile, writeTile} from '../test/fixtures/vector_tile.js';
import Pbf from '../index.js';
import {PbfReader, PbfWriter} from '../index.js';

var data = fs.readFileSync(new URL('../test/fixtures/12665.vector.pbf', import.meta.url)),
const data = fs.readFileSync(new URL('../test/fixtures/12665.vector.pbf', import.meta.url)),
suite = new Benchmark.Suite(),
vtProtoUrl = new URL('../test/fixtures/vector_tile.proto', import.meta.url),
ProtocolBuffersTile = protocolBuffers(fs.readFileSync(vtProtoUrl)).Tile,
ProtobufjsTile = protobufjs.loadSync(fileURLToPath(vtProtoUrl)).lookup('vector_tile.Tile');

var pbfTile = readTile(new Pbf(data)),
const pbfTile = readTile(new PbfReader(data)),
tileJSON = JSON.stringify(pbfTile),
protocolBuffersTile = ProtocolBuffersTile.decode(data),
protobufjsTile = ProtobufjsTile.decode(data);

suite
.add('decode vector tile with pbf', function() {
readTile(new Pbf(data));
.add('decode vector tile with pbf', () => {
readTile(new PbfReader(data));
})
.add('encode vector tile with pbf', function() {
var pbf = new Pbf();
.add('encode vector tile with pbf', () => {
const pbf = new PbfWriter();
writeTile(pbfTile, pbf);
pbf.finish();
})
.add('decode vector tile with protocol-buffers', function() {
.add('decode vector tile with protocol-buffers', () => {
ProtocolBuffersTile.decode(data);
})
.add('encode vector tile with protocol-buffers', function() {
.add('encode vector tile with protocol-buffers', () => {
ProtocolBuffersTile.encode(protocolBuffersTile);
})
.add('decode vector tile with protobuf.js', function() {
.add('decode vector tile with protobuf.js', () => {
ProtobufjsTile.decode(data);
})
.add('encode vector tile with protobuf.js', function() {
.add('encode vector tile with protobuf.js', () => {
ProtobufjsTile.encode(protobufjsTile);
})
.add('JSON.parse vector tile', function() {
.add('JSON.parse vector tile', () => {
JSON.parse(tileJSON);
})
.add('JSON.stringify vector tile', function() {
.add('JSON.stringify vector tile', () => {
JSON.stringify(pbfTile);
})
.on('cycle', function(event) {
.on('cycle', (event) => {
console.log(String(event.target));
})
.run();
Loading
Loading