protobufjs vs google-protobuf vs ts-proto
Serializing Data with Protocol Buffers in TypeScript
protobufjsgoogle-protobufts-protoSimilar Packages:

Serializing Data with Protocol Buffers in TypeScript

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
protobufjs84,032,02310,5853.76 MB833 days agoBSD-3-Clause
google-protobuf6,453,162470927 kB706 months ago(BSD-3-Clause AND Apache-2.0)
ts-proto1,857,5932,590798 kB1729 days agoISC

Serializing Data with Protocol Buffers in TypeScript

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.

🛠️ Code Generation: How You Get Started

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.

  • You must install the protoc compiler separately.
  • The output is JavaScript classes that extend the library's base Message class.
# 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.

  • You can generate static code (like google-protobuf) or load files dynamically at runtime.
  • Static generation is preferred for frontend builds to avoid bundling the parser.
# 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.

  • It generates TypeScript files with interfaces and helper functions.
  • It focuses on making the generated code feel like native 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: How TypeScript Feels

Type safety is critical for maintaining large codebases without runtime errors.

google-protobuf provides types via external definition files or JSDoc.

  • The generated code is primarily JavaScript.
  • You often need separate .d.ts files or rely on inference that feels less precise.
  • Accessing fields usually requires getter/setter methods.
// google-protobuf: Accessing fields
msg.getId(); // Getter method
msg.setId(123); // Setter method

protobufjs supports TypeScript but can be verbose.

  • Static code generation includes types, but the API uses a mix of objects and classes.
  • You may need to cast results or handle null values explicitly.
// protobufjs: Accessing fields
msg.id; // Direct property access
msg.id = 123;

ts-proto generates full TypeScript interfaces by default.

  • The generated code looks and feels like standard TypeScript.
  • It supports optional fields, unions, and other TS features naturally.
  • Editors provide excellent autocomplete and error checking.
// ts-proto: Accessing fields
msg.id; // Direct property access with full type safety

📦 Serialization: Encoding and Decoding

Once you have your objects, you need to convert them to binary for transport.

google-protobuf uses methods on the message instance.

  • Serialization is tied to the class instance.
  • You call methods like 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.

  • You pass the object to the encode function.
  • You must call .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.

  • It often wraps the underlying runtime logic.
  • The API is clean and consistent with the generated types.
// ts-proto: Encoding
const bytes = MyMessage.encode(msg).finish();

// ts-proto: Decoding
const loaded = MyMessage.decode(bytes);

🌐 Runtime Dependencies: What Gets Bundled

The size and behavior of your bundle depend on what library code is included.

google-protobuf includes its own runtime library.

  • This runtime is optimized for correctness but can be heavier.
  • It is a separate dependency that must be shipped to the client.

protobufjs includes a pure JavaScript runtime.

  • It is known for being lightweight and tree-shakeable.
  • You can choose minimal builds to reduce bundle size further.

ts-proto typically relies on protobufjs for the actual serialization.

  • This means you get the protobufjs runtime in your bundle anyway.
  • The benefit is the generated TypeScript layer, not a new runtime.

🤝 Similarities: Shared Ground

Despite their differences, these libraries share core capabilities.

1. 📄 Based on .proto Files

  • All three start with standard Protocol Buffer definition files.
  • This ensures compatibility with backends written in Go, Java, or Python.
// Shared definition for all libraries
message MyMessage {
  int32 id = 1;
  string name = 2;
}

2. 🚀 Binary Serialization

  • All produce compact binary output instead of verbose JSON.
  • This reduces network payload size significantly for large datasets.
// All produce Uint8Array
const bytes = encode(message); // Works for all three

3. 🔍 Field Validation

  • All enforce the schema defined in your .proto files.
  • Missing required fields or wrong types will cause errors during encoding or decoding.
// All will throw or error if schema is violated
try {
  decode(invalidBytes);
} catch (e) {
  // Handle schema mismatch
}

4. 🛠️ CLI Tooling

  • All rely on command-line tools to generate code from definitions.
  • This step is usually part of your build pipeline or CI process.
# All use protoc or custom CLI
protoc --out=. messages.proto

5. 🌍 Cross-Platform Support

  • All work in Node.js and modern browsers.
  • They handle endianness and binary data consistently across environments.
// All work in browser console or Node REPL
console.log(typeof window !== 'undefined'); // true

📊 Summary: Key Differences

Featuregoogle-protobufprotobufjsts-proto
Primary FocusOfficial Reference ImplementationPure JS RuntimeTypeScript Code Generation
Type SafetyModerate (JS with externs)Good (Static types)Excellent (Native TS)
API StyleGetters/Setters (getId())Property Access (id)Property Access (id)
RuntimeOwn RuntimeOwn RuntimeUses protobufjs Runtime
Setupprotoc + JS Pluginpbjs or protocprotoc + TS Plugin
Best ForLegacy/Strict ComplianceFlexibility/PerformanceModern TypeScript Apps

💡 Final Recommendation

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.

How to Choose: protobufjs vs google-protobuf vs ts-proto

  • protobufjs:

    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.

  • google-protobuf:

    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.

  • ts-proto:

    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.

README for protobufjs

protobuf.js
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 started

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.

Install

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.

Browser builds

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.

Usage

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.

Load a schema

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().

Encode and decode

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.

Convert plain objects

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:

OptionEffect
longs: BigIntConverts 64-bit values to bigint values
longs: StringConverts 64-bit values to decimal strings
longs: NumberConverts 64-bit values to JS numbers (may lose precision)
enums: StringConverts enum values to names
bytes: StringConverts bytes to base64 strings
defaults: trueIncludes default values for unset fields
arrays: trueIncludes empty arrays for repeated fields
objects: trueIncludes empty objects for map fields
oneofs: trueIncludes virtual oneof discriminator properties

Message API

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.

Code generation

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.

TargetOutputRuntime entry
json-moduleReflection moduleprotobufjs/light.js (without parser)
static-moduleStatic code moduleprotobufjs/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

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

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().

TypeScript integration

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"
};

Advanced usage

Programmatic schemas

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);

Custom message classes

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.

Services

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.

Extensions

The following extensions provide descriptor conversion and text-based protobuf formats when reflection metadata is available. Most applications only need the binary APIs above.

Descriptors

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.

ProtoJSON

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.

Text Format

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.

Conformance

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.

CategoryTotalRequiredRecommended
Binary100.00% (2835/2835)100.00% (1958/1958)100.00% (877/877)
↳ Proto2100.00% (707/707)100.00% (489/489)100.00% (218/218)
↳ Proto3100.00% (707/707)100.00% (486/486)100.00% (221/221)
↳ Editions100.00% (1421/1421)100.00% (983/983)100.00% (438/438)
ProtoJSON100.00% (2814/2814)100.00% (2362/2362)100.00% (452/452)
TextFormat100.00% (909/909)100.00% (845/845)100.00% (64/64)
Overall100.00% (6558/6558)100.00% (5165/5165)100.00% (1393/1393)
EditionSupported since
2023v7.5.0
2024v8.0.0
2026v8.8.0

Structured results of the conformance tests are also available as CI artifacts.

Performance

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.

Encode Throughput
Show table
ImplementationCommonVector tileBuf perf
protobuf.js static5.28M ops/s   1.0x3.06K ops/s   1.0x49.2K ops/s   1.0x
protobuf.js reflect5.16M ops/s   1.0x3.08K ops/s   1.0x47.5K ops/s   1.0x
JSON3.44M ops/s   1.5x1.50K ops/s   2.1x7.13K ops/s   6.9x
protoc-gen-js1.05M ops/s   5.0x691 ops/s   4.5x15.0K ops/s   3.3x
protoc-gen-es1.22M ops/s   4.3x1.14K ops/s   2.7x35.8K ops/s   1.4x
Decode Throughput
Show table
ImplementationCommonVector tileBuf perf
protobuf.js static5.84M ops/s   1.1x2.45K ops/s   1.1x78.9K ops/s   1.0x
protobuf.js reflect6.54M ops/s   1.0x2.66K ops/s   1.0x77.0K ops/s   1.0x
JSON1.58M ops/s   4.1x1.21K ops/s   2.2x21.0K ops/s   3.8x
protoc-gen-js701K ops/s   9.3x946 ops/s   2.8x21.5K ops/s   3.7x
protoc-gen-es1.67M ops/s   3.9x1.19K ops/s   2.2x31.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

Compatibility

Supported runtimes are browsers, Node.js v12+, Deno and Bun. When using the CLI with Bun, Node.js must also be installed.

Security

Security-impacting reports are handled through coordinated GitHub Security Advisories where appropriate. See SECURITY.md for supported release lines and reporting instructions.

Development

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

Additional documentation