Open-source project
kriszyp/msgpackr avatar
kriszyp/msgpackr

msgpackr: a MessagePack implementation that reuses record structures

Ultra-fast MessagePack implementation with extension for record and structural cloning / msgpack.org[JavaScript/NodeJS]

694 stars72 forksJavaScriptMIT

At a glance

What is it?
msgpackr is a MessagePack codec for Node and JavaScript that adds a record extension, so objects with the same shape are encoded once and referenced afterwards, which makes the payloads smaller and the decode faster, with optional structured cloning for cyclic and typed objects. It suits a Node service moving structured data over a socket, and it is the wrong tool if the other end of the wire only speaks standard MessagePack.
Who is it for?
msgpackr fits a Node service that serialises the same object shapes over and over, since the record extension encodes a structure once and references it, which cuts both payload size and decode time, and the Packr and Unpackr streams make that work directly on sockets and pipes.
Can I use it commercially?
Yes. MIT 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 1 day 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

The record extension, and why it changes the payload size

msgpackr is a MessagePack implementation for NodeJS and JavaScript, and the distinguishing feature is the optional record extension, which is the r in the name.

The problem it addresses is the difference between a map that holds an arbitrary set of keys, which is what a JavaScript Map is for, and a record or object structure with a well-defined set of fields. The documentation points out that typical JavaScript objects and records reuse the same structure across many instances, and that the record extension preserves that distinction inside MessagePack. The effect is that structures are reused rather than re-encoded, which gives better type preservation, more compact encodings, and decoding that is two to three times faster.

The implementation is automatic. Record definitions are generated by the library and reused, and objects with the same structure reference the same definition. For a payload with large object structures and repeating nested objects of similar shape, the documentation says that simply serializing with the record extension yields significant benefits. You do not have to declare your schemas.

Using it means creating a Packr instance rather than calling the free functions, and the record extension is enabled by default on a new instance:

js
import { Packr } from 'msgpackr';
let packr = new Packr();

The plain functions are still there and are still standard. The pack function generates standard MessagePack without any extensions, so it should be compatible with any standard MessagePack parser, and it serialises JavaScript objects as MessagePack maps. The unpack function deserialises MessagePack maps as an Object with the map's properties. That distinction is the one to keep in mind: the free functions are interoperable, and the Packr instance is where the speed comes from, at the cost of using a non-standard extension.

Streams, and the structured clone option you probably should leave off

The library includes streaming classes that go beyond the plain encode and decode calls. PackrStream is a NodeJS transform stream that serialises objects to a binary stream, for writing to a network or socket or for inter-process communication, and UnpackrStream does the reverse. The streaming path is described as further improving performance, and both stream instances have the record structure extension enabled by default.

The example in the documentation pipes one stream into the other, which is a convenient way to see the shape of the API:

js
import { PackrStream, UnpackrStream } from 'msgpackr';
let sendingStream = new PackrStream();
let receivingStream = new UnpackrStream();
sendingStream.pipe(receivingStream);
sendingStream.write(myData);
receivingStream.on('data', (data) => {

The other optional feature is structured cloning, which is enabled with the structuredClone option. It lets you include references to other objects or cyclic references while preserving object identity, and it preserves certain typed objects such as Error, Set, RegExp and TypedArray instances. The example creates a self-referencing object with a Set and a regular expression, packs it, unpacks it, and checks that copy.self is copy.

The option is disabled by default, and the reason is a performance cost of about 25 to 30% because it uses extensions and reference checking. There is also a caveat that the implementation does not serialise every class or type in the HTML specification, since not all of them make sense for storing across platforms.

There is also a naming convenience. The library exports aliases so that encoder and decoder terminology works too: decode is equivalent to unpack, encode is pack, Encoder is Packr, Decoder is Unpackr, and EncoderStream and DecoderStream are available as well. That matters for teams whose existing code or conventions use one vocabulary.

One package, four runtimes, and the exports map that decides

Installing is a single command:

bash
npm i msgpackr

The package runs on any modern JavaScript platform but is optimised for NodeJS, and it will use a node addon for a performance boost as an optional dependency. That optional dependency is worth understanding: the fast path is a native addon, so a deployment that forbids native modules, or a platform the addon does not build for, silently takes the pure JavaScript path instead.

The exports map in package.json is the part to read before wiring it into a build. The root entry resolves differently by condition. For Node, require resolves to ./dist/node.cjs and import to ./node-index.js. Bun gets the same pair. The browser condition points at ./index.js, and there is a default fallback to ./index.js as well. The types are split too, with index.d.cts for require and index.d.ts for import.

The subpath exports matter for bundle size. There is a separate ./pack entry and a separate ./unpack entry, and the documentation recommends importing the module of interest directly to minimise dependencies pulled into the application, with the example showing an import of unpack from msgpackr/unpack if that is all you need. Each of those subpaths has its own condition map, with the node require case pointing at the compiled CommonJS bundle and the browser case at the plain JavaScript file.

There are subpath imports for the pack and unpack halves, and the repository root carries the source files that back them: pack.js, unpack.js, index.js, node-index.js, stream.js, iterators.js, plus the TypeScript declarations pack.d.ts, unpack.d.ts and index.d.ts. The build script runs rollup and then copies the declaration files to .d.cts variants, which is how the types are provided for a CommonJS consumer of an ESM-first package.

Browser builds, Deno, Bun, and the CSP problem

The library covers the runtimes a JavaScript project actually has to worry about.

For Deno, the modules are standard ESM and can be loaded from the deno.land registry for msgpackr, or through the NPM module loader with an import from npm:msgpackr. The standard pack and unpack functionality is available there, and Bun is described as being usable like any other package.

In the browser, msgpackr works as standalone JavaScript on modern browsers. There is a bundled script at dist/index.js for direct loading through a script tag, which is UMD based and registers as a module if possible or creates a msgpackr global with all the exported functions. A minified bundle is included as index.min.js, and there is a third build, index-no-eval.js, that excludes dynamic code evaluation.

That last build is the interesting one. The dynamic evaluation provides important performance optimisations for records, so it is not recommended unless a Content Security Policy forbids eval and Function in code. In other words, a strict CSP costs you the record extension's main advantage, and the package ships the alternative so the choice is explicit rather than a mysterious slowdown.

The JavaScript files at the repository root are the source of truth for all of this: index.js is the browser and default entry, node-index.js is the Node ESM entry, and rollup.config.js produces the bundles, with the dist directory and index.min.js derived from them.

The NodeJS addon is the performance story for server use, and the browser bundles are the compatibility story for client use, and the two paths are genuinely different code rather than one build with a flag.

Where msgpackr is the wrong choice

The first limitation is interoperability, and it is the one that decides the question. The record extension is not part of the MessagePack specification, so a payload encoded by a Packr instance with records enabled cannot be read by a standard parser. If the other end of your wire is a third-party service, a client in another language, or a future version of your own service that has switched to the plain pack function, the record path is a locked door. The documentation is clear that the free pack function produces standard output, and that distinction is the whole design.

The second is the cost of structured cloning. The 25 to 30% performance reduction is the documented price of the structuredClone option, and the same trade appears again in the browser with the no-eval build. Both features that extend the format beyond MessagePack cost performance, and the plain functions are the fast path.

The third is that the fast path is native. On Node the addon is an optional dependency, so the speed depends on a prebuilt binary being available for your platform, and the pure JavaScript fallback is what runs otherwise. On a platform where the addon has to be compiled, that is a build dependency you may not want in a container image, and the performance gap between the two paths is not something the documentation quantifies for your workload.

The fourth is maintenance pace. The visible release tags for this repository are old, v1.3.2 from 2021-05-18 and v1.3.0 from 2021-05-14, while the package.json records the version as 2.1.2 and the last push was on 2026-10-20. The tags are clearly not tracking the published package, so anyone who wants to know what changed in a version has to read the changelog on npm rather than the GitHub release list, and the discrepancy is a small sign that the release tagging and the publish pipeline are separate.

The fifth is that MessagePack is not always the right format. It is a binary format with no self-describing schema, so a payload that crosses an organisational boundary and needs to be read by something you do not control is a poor fit, and JSON remains the better answer there.

Editorial conclusion

msgpackr fits a Node service that serialises the same object shapes over and over, since the record extension encodes a structure once and references it, which cuts both payload size and decode time, and the Packr and Unpackr streams make that work directly on sockets and pipes. It is the wrong choice when the consumer is a third party that only parses standard MessagePack, since the record extension is not part of the specification, and it is the wrong choice for a browser app with a strict Content Security Policy unless you take the no-eval build and give up the record optimisation. Before adopting it, measure your own payload rather than the published benchmark numbers, and check the exports map, because the entry point changes between Node, Bun, the browser and a CommonJS require.

Frequently asked questions

How do I install msgpackr?

Run npm i msgpackr. The package works on any modern JavaScript platform and is optimised for NodeJS, where it uses a node addon as an optional dependency for a performance boost.

What is the record extension in msgpackr?

It is an optional MessagePack extension, enabled by default on a new Packr instance, that encodes a record structure once and references it for objects with the same shape. That yields more compact encodings and decoding that is two to three times faster, at the cost of producing output that a standard MessagePack parser cannot read.

Does msgpackr support structured cloning with cyclic references?

Yes, through the structuredClone option, which preserves object identity and certain typed objects such as Error, Set, RegExp and TypedArray instances. It is disabled by default because reference checking degrades performance by about 25 to 30%.

How do I use msgpackr in a browser with a strict Content Security Policy?

Use the index-no-eval.js bundle, which excludes dynamic code evaluation. The package warns that this build gives up the performance optimisations for records, so it is not recommended unless the policy requires it.

What licence is msgpackr released under?

MIT, with the licence file at the root of the repository alongside a SECURITY.md file.

Official sources

  1. Issues
  2. kriszyp/msgpackr on GitHub
  3. License: MIT
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kriszyp-msgpackr.svg)](https://hysenlabs.com/projects/kriszyp-msgpackr)

Was this useful?