google-protobuf, protobufjs, and ts-proto are tools for working with Protocol Buffers in JavaScript and TypeScript environments. google-protobuf is the official runtime library from Google, providing strict compliance with the protocol. protobufjs is a community-driven pure JavaScript implementation known for flexibility and performance. ts-proto is a code generator that produces TypeScript types and helper functions, often leveraging protobufjs under the hood. Together, they enable efficient binary serialization for web applications, though they differ in type safety, setup complexity, and runtime behavior.
When building high-performance web applications, sending data as binary instead of JSON can save bandwidth and improve speed. Protocol Buffers (Protobuf) are a popular way to handle this. The three main tools for using Protobuf in JavaScript are google-protobuf, protobufjs, and ts-proto. They all solve the same problem but take different approaches to code generation, type safety, and runtime behavior. Let's look at how they compare in real engineering scenarios.
The first step with any Protobuf library is turning your .proto definition files into code you can import.
google-protobuf uses the official protoc compiler with a specific JavaScript plugin.
protoc compiler separately.# google-protobuf: Generate JS code
protoc --js_out=import_style=commonjs,binary:. messages.proto
// google-protobuf: Import generated code
const { MyMessage } = require('./messages_pb');
const msg = new MyMessage();
protobufjs offers its own CLI tool (pbjs) or works with protoc.
google-protobuf) or load files dynamically at runtime.# protobufjs: Generate static JS/TS code
pbjs -t static-module -w commonjs -o messages.js messages.proto
// protobufjs: Import generated code
const { MyMessage } = require('./messages');
const msg = MyMessage.create({ id: 1 });
ts-proto is a plugin for protoc designed specifically for TypeScript.
# ts-proto: Generate TS code
protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=. messages.proto
// ts-proto: Import generated code
import { MyMessage } from './messages';
const msg = { id: 1 }; // Plain object often works
Type safety is critical for maintaining large codebases without runtime errors.
google-protobuf provides types via external definition files or JSDoc.
.d.ts files or rely on inference that feels less precise.// google-protobuf: Accessing fields
msg.getId(); // Getter method
msg.setId(123); // Setter method
protobufjs supports TypeScript but can be verbose.
null values explicitly.// protobufjs: Accessing fields
msg.id; // Direct property access
msg.id = 123;
ts-proto generates full TypeScript interfaces by default.
// ts-proto: Accessing fields
msg.id; // Direct property access with full type safety
Once you have your objects, you need to convert them to binary for transport.
google-protobuf uses methods on the message instance.
serializeBinary directly on the object.// google-protobuf: Encoding
const bytes = msg.serializeBinary(); // Returns Uint8Array
// google-protobuf: Decoding
const loaded = MyMessage.deserializeBinary(bytes);
protobufjs uses static helper methods on the class.
encode function..finish() on the writer to get the final buffer.// protobufjs: Encoding
const bytes = MyMessage.encode(msg).finish();
// protobufjs: Decoding
const loaded = MyMessage.decode(bytes);
ts-proto also uses static helper methods, similar to protobufjs.
// ts-proto: Encoding
const bytes = MyMessage.encode(msg).finish();
// ts-proto: Decoding
const loaded = MyMessage.decode(bytes);
The size and behavior of your bundle depend on what library code is included.
google-protobuf includes its own runtime library.
protobufjs includes a pure JavaScript runtime.
ts-proto typically relies on protobufjs for the actual serialization.
protobufjs runtime in your bundle anyway.Despite their differences, these libraries share core capabilities.
.proto Files// Shared definition for all libraries
message MyMessage {
int32 id = 1;
string name = 2;
}
// All produce Uint8Array
const bytes = encode(message); // Works for all three
.proto files.// All will throw or error if schema is violated
try {
decode(invalidBytes);
} catch (e) {
// Handle schema mismatch
}
# All use protoc or custom CLI
protoc --out=. messages.proto
// All work in browser console or Node REPL
console.log(typeof window !== 'undefined'); // true
| Feature | google-protobuf | protobufjs | ts-proto |
|---|---|---|---|
| Primary Focus | Official Reference Implementation | Pure JS Runtime | TypeScript Code Generation |
| Type Safety | Moderate (JS with externs) | Good (Static types) | Excellent (Native TS) |
| API Style | Getters/Setters (getId()) | Property Access (id) | Property Access (id) |
| Runtime | Own Runtime | Own Runtime | Uses protobufjs Runtime |
| Setup | protoc + JS Plugin | pbjs or protoc | protoc + TS Plugin |
| Best For | Legacy/Strict Compliance | Flexibility/Performance | Modern TypeScript Apps |
google-protobuf is the official choice 🏛️ — best for teams that need strict alignment with Google's ecosystem or are maintaining older systems. It is reliable but feels less modern in TypeScript projects.
protobufjs is the flexible workhorse 🛠️ — ideal for JavaScript-heavy projects or when you need dynamic loading of proto files. It balances performance and ease of use well.
ts-proto is the modern standard ✨ — the top pick for new TypeScript applications. It provides the best developer experience by generating clean types and integrating smoothly with your existing tooling.
Final Thought: For most frontend teams today, ts-proto combined with the protobufjs runtime offers the best balance of safety and performance. Use google-protobuf only if you have a specific requirement for the official runtime.
Choose protobufjs if you need a pure JavaScript solution that supports both dynamic and static code generation without heavy dependencies. It is ideal for teams that want flexibility in loading .proto files at runtime or need a proven, high-performance library with broad community support. This package works well when you want control over the serialization process without extra code generation steps.
Choose google-protobuf if you require strict official compliance with Google's reference implementation or need to interoperate with legacy systems already using this runtime. It is suitable for projects where matching the behavior of other Google language bindings exactly is more important than TypeScript developer experience. Be aware that it may feel less native to JavaScript ecosystems compared to alternatives.
Choose ts-proto if you are building a modern TypeScript application and want the best possible type safety and developer experience. It generates clean TypeScript interfaces and functions that integrate seamlessly with your editor and build tools. This is the preferred choice for new projects where type correctness and ease of maintenance are top priorities.
protobuf.js
protobuf.js is a very fast, conformant, and unusually versatile JavaScript implementation of Protocol Buffers for Node.js and browsers. It is independently maintained with contributions from the upstream Protocol Buffers project, works with .proto schemas without requiring protoc, and supports runtime reflection as well as specialized code generation with matching TypeScript declarations.
If protobuf.js is important to your project or organization, or if you depend on it commercially, consider supporting its ongoing maintenance. Sponsorship helps make bug fixes, releases, LTS/security handling, and user support more sustainable.
Getting up and running is simple: Install the package, load a schema, and you are all set to encode and decode Protobuf messages. From there, protobuf.js grows with your requirements: Add code generation with TypeScript declarations, transport-agnostic services, support for text-based formats, and more as needed.
npm install protobufjs
The command line utility for generating reflection bundles, static code and TypeScript declarations is published as an add-on package:
npm install --save-dev protobufjs-cli
The CLI is a JS-native protobuf.js toolchain that does not require setting up protoc. If you prefer a protoc-based workflow, it provides protoc-gen-pbjs as an option.
Canonical browser builds are provided via the jsDelivr CDN, supporting CommonJS, AMD and global window.protobuf. Make sure to pin an exact version in production.
The examples below use this schema:
syntax = "proto3";
package awesomepackage;
message AwesomeMessage {
string awesome_field = 1;
}
protobuf.js converts .proto field names to camelCase by default, so awesome_field is used as awesomeField in JavaScript. Use the keepCase option when loading or parsing .proto files to preserve field names as written.
const protobuf = require("protobufjs");
const root = await protobuf.load("awesome.proto");
const AwesomeMessage = root.lookupType("awesomepackage.AwesomeMessage");
Optionally use load() with a callback, or loadSync() for synchronous loading on Node.js. Imports resolve relative to the importing file by default. To resolve imports against a specific base directory, create a Root and override root.resolvePath before calling root.load().
const payload = { awesomeField: "hello" };
// Optionally create a message instance from already valid data
const message = AwesomeMessage.create(payload);
const encoded = AwesomeMessage.encode(message).finish();
const decoded = AwesomeMessage.decode(encoded);
encode expects a message instance or equivalent plain object and does not verify input implicitly. Use create to create a message instance from already valid data when useful, verify for plain objects whose shape is not guaranteed, and fromObject when conversion from broader JavaScript objects is needed.
Plain objects can be encoded directly when they already use protobuf.js runtime types: numbers for 32-bit numeric fields, booleans for bool, strings for string, Uint8Array or Buffer for bytes, arrays for repeated fields, and plain objects for maps. Map keys are the string representation of the respective value or an 8-character hash string for 64-bit keys.
Note that, as with any structured binary format, decoded structures incur runtime memory overhead beyond their encoded representation. Applications processing untrusted input should therefore apply appropriate input-size and concurrency limits to bound memory use. For this reason, unknown fields present on the wire are intentionally discarded as the safer default, but can be retained by setting reader.discardUnknown = false per reader, or by setting Reader.discardUnknown = false to make it the default for subsequently created readers. Preserved unknown field data can also be explicitly dropped with delete message.$unknowns.
Conversion is an explicit interoperability boundary. fromObject accepts common JavaScript inputs such as enum values by name, base64 bytes, decimal 64-bit strings, Long, and BigInt; toObject lets callers choose the output expected by their application or transport.
const message = AwesomeMessage.fromObject({ awesomeField: 42 });
const object = AwesomeMessage.toObject(message, {
longs: String,
enums: String,
bytes: String
});
Common ConversionOptions are:
| Option | Effect |
|---|---|
longs: BigInt | Converts 64-bit values to bigint values |
longs: String | Converts 64-bit values to decimal strings |
longs: Number | Converts 64-bit values to JS numbers (may lose precision) |
enums: String | Converts enum values to names |
bytes: String | Converts bytes to base64 strings |
defaults: true | Includes default values for unset fields |
arrays: true | Includes empty arrays for repeated fields |
objects: true | Includes empty objects for map fields |
oneofs: true | Includes virtual oneof discriminator properties |
Message types expose focused methods for validation, conversion, and binary I/O.
encode(message: Message | object, writer?: Writer): Writer
Encodes a message or equivalent plain object. Call .finish() on the returned writer to obtain a buffer.
encodeDelimited(message: Message | object, writer?: Writer): Writer
Encodes a length-delimited message.
decode(reader: Reader | Uint8Array): Message
Decodes a message from protobuf binary data.
decodeDelimited(reader: Reader | Uint8Array): Message
Decodes a length-delimited message.
create(properties?: object): Message
Creates a message instance from already valid data.
verify(object: object): null | string
Checks whether a plain object can be encoded as-is. Returns null if valid, otherwise an error message.
fromObject(object: object): Message
Converts broader JavaScript input into a message instance.
toObject(message: Message, options?: ConversionOptions): object
Converts a message instance to a configurable plain JavaScript object.
message.toJSON(): object
Converts a message instance to JSON-compatible output using default conversion options.
Message instances provide stable runtime identity for testing with instanceof.
Length-delimited methods read and write a varint byte length before the message, which is useful for streams and framed protocols.
If required fields are missing while decoding proto2 data, decode throws protobuf.util.ProtocolError with the partially decoded message available as err.instance.
Use protobufjs-cli to generate schema-specific code, either directly with pbjs or through the optional protoc-gen-pbjs plugin for protoc.
protobuf.js offers two code-generation modes, both with matching TypeScript declarations: reflection modules generate optimized code at runtime with reflection metadata retained; static modules generate reflection-free code ahead of time. Both use the same runtime package and automatically import only the runtime capabilities they need.
| Target | Output | Runtime entry |
|---|---|---|
json-module | Reflection module | protobufjs/light.js (without parser) |
static-module | Static code module | protobufjs/minimal.js (without reflection) |
Module targets support --wrap default for CommonJS and AMD, plus esm, commonjs, amd, and closure; --wrap can also load a custom wrapper module.
Static modules emit dedicated, reflection-free JavaScript code for your schema.
npx pbjs -t static-module -w esm -o awesome.js --dts awesome.proto
import { awesomepackage } from "./awesome.js";
const message = awesomepackage.AwesomeMessage.create({ awesomeField: "hello" });
Static code is repetitive by design, but compresses unusually well with Brotli or gzip and works in CSP-restricted environments.
Reflection modules wrap schemas as compact JSON metadata while avoiding .proto parsing at runtime.
npx pbjs -t json-module -w esm -o awesome.js --dts awesome.proto
import { awesomepackage } from "./awesome.js";
const AwesomeMessage = awesomepackage.AwesomeMessage;
Declarations for reflection modules mirror static-module typings. Because JSON modules export reflection objects, message instances should be created with MyMessage.create(...) rather than constructors. Code using create(...) works with static modules as well. Separately, -t json can serialize reflection metadata as bare JSON bundles for use with load() or Root.fromJSON().
protobuf.js works with TypeScript out of the box: its runtime API is typed, and generated code can be paired with matching TypeScript declarations in a single CLI invocation. Declarations are strongly typed, including discriminated unions for oneofs and scoped types for plain-object usage:
message Profile {
oneof contact {
string email = 1;
string phone = 2;
}
}
const profile = Profile.create({
contact: "email",
email: "hello@example.com"
});
if (profile.contact === "email") {
profile.email; // string
}
const decoded = Profile.decode(bytes);
if (decoded.contact === "phone") {
decoded.phone; // string
}
The same narrowed shape is available for plain-object inputs:
const object: Profile.$Shape = {
contact: "email",
email: "hello@example.com"
};
Schemas can be constructed directly through reflection:
const AwesomeMessage = new protobuf.Type("AwesomeMessage")
.add(new protobuf.Field("awesomeField", 1, "string"));
const root = new protobuf.Root()
.define("awesomepackage")
.add(AwesomeMessage);
A reflected type can use a custom class as its runtime constructor:
class AwesomeMessage extends protobuf.Message<AwesomeMessage> {
awesomeField = "";
constructor(properties?: protobuf.Properties<AwesomeMessage>) {
super(properties);
// ...
}
customInstanceMethod() {
return this.awesomeField.toLowerCase();
}
}
root.lookupType("awesomepackage.AwesomeMessage").ctor = AwesomeMessage;
const decoded = AwesomeMessage.decode(bytes);
decoded.customInstanceMethod(); // string
protobuf.js will populate the constructor with the usual static runtime methods and use it for decoded messages. When assigning constructors manually, add the type to its parent namespace/root first if fields reference other reflected types. In TypeScript, custom members are visible when using the custom class type in consuming code.
protobuf.js supports service clients built from service definitions. The service API is transport-agnostic: provide an rpcImpl function to connect it to HTTP, WebSocket, gRPC, or another transport.
function myRpcImpl(method, requestData, callback) {
// method.name
// method.path
// method.requestStream?
// method.responseStream?
performRequest(requestData, function(err, responseData) {
callback(err, responseData);
});
}
const myService = MyService.create(myRpcImpl/*, requestDelimited?, responseDelimited? */);
See examples/streaming-rpc.js for a streaming example.
See examples/grpc-service.js for an integration example with @grpc/grpc-js.
The following extensions provide descriptor conversion and text-based protobuf formats when reflection metadata is available. Most applications only need the binary APIs above.
protobuf.js uses a compact JSON-based reflection representation internally. See ext/descriptor for use cases that need conversion between reflected roots and protoc descriptor messages.
Protocol Buffers support a special ProtoJSON format to share data with systems that do not support the binary wire format, for example when implementing gateways. Spec-compliant ProtoJSON is supported via ext/protojson.
Protocol Buffers Text Format is a special syntax for representing protobuf data in text form, which can be useful for configurations or tests. Spec-compliant Text Format is supported via ext/textformat.
protobuf.js is validated against the official Protocol Buffers conformance suite. It passes all required and recommended tests for the Proto2, Proto3 and Editions binary wire formats, with complete ProtoJSON and Text Format support available as optional extensions when those formats are needed.
| Category | Total | Required | Recommended |
|---|---|---|---|
| Binary | 100.00% (2835/2835) | 100.00% (1958/1958) | 100.00% (877/877) |
| ↳ Proto2 | 100.00% (707/707) | 100.00% (489/489) | 100.00% (218/218) |
| ↳ Proto3 | 100.00% (707/707) | 100.00% (486/486) | 100.00% (221/221) |
| ↳ Editions | 100.00% (1421/1421) | 100.00% (983/983) | 100.00% (438/438) |
| ProtoJSON | 100.00% (2814/2814) | 100.00% (2362/2362) | 100.00% (452/452) |
| TextFormat | 100.00% (909/909) | 100.00% (845/845) | 100.00% (64/64) |
| Overall | 100.00% (6558/6558) | 100.00% (5165/5165) | 100.00% (1393/1393) |
| Edition | Supported since |
|---|---|
| 2023 | v7.5.0 |
| 2024 | v8.0.0 |
| 2026 | v8.8.0 |
Structured results of the conformance tests are also available as CI artifacts.
protobuf.js's reflection and static modes share the same code-generation backend and underlying hand-tuned reader and writer primitives. Reflection mode emits specialized encoders and decoders just in time, while static mode emits equivalent code ahead of time.
To see how this architecture compares in practice, the repository includes a reproducible benchmark suite measuring both modes alongside other general-purpose JavaScript implementations across three cases: our classic common message shape and two unmodified, structurally distinct fixtures sourced from other implementations. Each library is tested through its recommended serialization and deserialization path using identical schemas and message contents.
| Implementation | Common | Vector tile | Buf perf |
|---|---|---|---|
| protobuf.js static | 5.28M ops/s 1.0x | 3.06K ops/s 1.0x | 49.2K ops/s 1.0x |
| protobuf.js reflect | 5.16M ops/s 1.0x | 3.08K ops/s 1.0x | 47.5K ops/s 1.0x |
| JSON | 3.44M ops/s 1.5x | 1.50K ops/s 2.1x | 7.13K ops/s 6.9x |
| protoc-gen-js | 1.05M ops/s 5.0x | 691 ops/s 4.5x | 15.0K ops/s 3.3x |
| protoc-gen-es | 1.22M ops/s 4.3x | 1.14K ops/s 2.7x | 35.8K ops/s 1.4x |
| Implementation | Common | Vector tile | Buf perf |
|---|---|---|---|
| protobuf.js static | 5.84M ops/s 1.1x | 2.45K ops/s 1.1x | 78.9K ops/s 1.0x |
| protobuf.js reflect | 6.54M ops/s 1.0x | 2.66K ops/s 1.0x | 77.0K ops/s 1.0x |
| JSON | 1.58M ops/s 4.1x | 1.21K ops/s 2.2x | 21.0K ops/s 3.8x |
| protoc-gen-js | 701K ops/s 9.3x | 946 ops/s 2.8x | 21.5K ops/s 3.7x |
| protoc-gen-es | 1.67M ops/s 3.9x | 1.19K ops/s 2.2x | 31.0K ops/s 2.5x |
Structured results include environment details for this run and are committed alongside the charts. The suite also runs in CI.
To run the benchmark on your own hardware:
npm --prefix bench install
npm --prefix bench run generate
npm run bench
Supported runtimes are browsers, Node.js v12+, Deno and Bun. When using the CLI with Bun, Node.js must also be installed.
Security-impacting reports are handled through coordinated GitHub Security Advisories where appropriate. See SECURITY.md for supported release lines and reporting instructions.
git clone https://github.com/protobufjs/protobuf.js
cd protobuf.js
npm install
npm --prefix cli install
Running the tests:
npm test
Building the development and production versions with their respective source maps to dist/:
npm run build