npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nxtedition/slice

v1.1.3

Published

A high-performance buffer slice and pool allocator for Node.js.

Readme

@nxtedition/slice

A high-performance buffer slice and pool allocator for Node.js.

Why

Node.js Buffer.subarray() is slow. Every call creates a new Buffer object — a typed array wrapper with prototype chain setup, internal slot initialization, and bounds validation. This overhead is negligible for occasional use, but becomes a bottleneck in hot paths — protocol parsers, binary codecs, streaming pipelines — where thousands of sub-views are created per second.

Buffer.allocUnsafe() is worse. Allocations above the pool size (Buffer.poolSize) threshold go through allocBuffer which crosses into C++ to create a new ArrayBuffer backing store. The pooled fast path still involves bookkeeping and pool management overhead, and every allocation produces a new Buffer object that the GC must eventually collect.

Slice avoids this entirely. It is a plain JavaScript object with buffer, byteOffset, and byteLength fields. Creating a slice is just setting three properties — no typed array wrapper creation, no GC pressure from short-lived Buffer objects. Operations like toString, copy, and compare delegate directly to the underlying buffer with the correct offsets.

PoolAllocator takes this further. Like Node's internal pool, it has management overhead — but it rarely (if ever) allocates new backing stores, and because Slice is a plain object rather than a typed array, resizing or freeing a slice doesn't produce garbage for V8 to collect. It pre-allocates a large contiguous buffer and hands out regions using power-of-2 bucketing. When a slice is freed, its slot is recycled. When a slice is resized within the same bucket, no data moves at all — just a field update. This gives you malloc/realloc/free semantics with near-zero overhead per operation. The trade-off is upfront memory allocation and internal fragmentation from power-of-2 rounding — a 10-byte allocation uses a 16-byte slot. Buckets are also independent: a freed 16-byte slot cannot satisfy a 32-byte request, so the pool can become fragmented if allocation sizes are uneven. Use stats to monitor pool utilization and tune the pool size for your workload.

Install

npm install @nxtedition/slice

Usage

import { Slice, PoolAllocator } from '@nxtedition/slice'

// Create a slice from an existing buffer
const buf = Buffer.from('hello world')
const slice = new Slice(buf, 6, 5)
slice.toString() // 'world'

// Use a pool allocator for high-throughput allocation
const pool = new PoolAllocator()
const s = new Slice()

pool.realloc(s, 64) // allocate 64 bytes from pool
s.write('hello')
pool.realloc(s, 128) // grow — may reuse same slot
pool.realloc(s, 0) // free — slot is recycled

Benchmarks

Measured on Apple M3 Pro, Node.js v25.3.0:

Allocation

| Operation | Buffer.allocUnsafe | Buffer.allocUnsafeSlow | PoolAllocator | Speedup | | ---------------- | -------------------: | -----------------------: | --------------: | ------: | | alloc 64 bytes | 38.08 ns | 41.23 ns | 5.66 ns | 6.7x | | alloc 256 bytes | 52.09 ns | 231.46 ns | 5.90 ns | 8.8x | | alloc 1024 bytes | 91.24 ns | 340.75 ns | 5.83 ns | 15.6x | | alloc 4096 bytes | 446.53 ns | 437.83 ns | 6.24 ns | 71.6x |

Allocation (GC)

| Operation | Buffer.allocUnsafe | Buffer.allocUnsafeSlow | PoolAllocator | Speedup | | ---------------- | -------------------: | -----------------------: | --------------: | ------: | | alloc 64 bytes | 400.46 ns | 167.94 ns | 6.33 ns | 63.3x | | alloc 256 bytes | 309.57 ns | 500.58 ns | 6.35 ns | 48.7x | | alloc 4096 bytes | 653.40 ns | 620.19 ns | 6.32 ns | 103.4x |

Under GC pressure, the advantage grows dramatically — up to 103x faster — because PoolAllocator reuses slots from a pre-allocated buffer and never creates objects for V8 to trace.

Slice creation vs Buffer.subarray

| Operation | Buffer.subarray | Slice | Speedup | | ---------------------- | ----------------: | -----------: | ------: | | subarray 64 bytes | 38.11 ns | 12.99 ns | 2.9x | | subarray 1024 bytes | 36.87 ns | 13.26 ns | 2.8x | | subarray 64 bytes (GC) | 127.30 ns | 81.60 ns | 1.6x |

Combined operations

| Operation | Buffer | PoolAllocator | Speedup | | ------------------------------------- | --------: | --------------: | ------: | | alloc/free 64 bytes | 32.72 ns | 30.80 ns | 1.1x | | alloc/free 64 bytes (GC) | 273.35 ns | 73.34 ns | 3.7x | | alloc/free 256 bytes | 58.88 ns | 29.38 ns | 2.0x | | realloc churn (64 → 128 → 64) | 93.63 ns | 26.99 ns | 3.5x | | realloc in-place (grow within bucket) | 60.16 ns | 11.06 ns | 5.4x | | 10 concurrent allocs then free | 406.26 ns | 337.83 ns | 1.2x | | 10 concurrent allocs then free (GC) | 647.95 ns | 649.73 ns | 1.0x |

API

Slice

A lightweight view over a Buffer with explicit offset and length tracking.

new Slice(buffer?: Buffer, byteOffset?: number, byteLength?: number, maxByteLength?: number)

Creates a new slice. All parameters are optional — defaults to an empty slice.

Properties

  • buffer: Buffer — The underlying Buffer
  • byteOffset: number — Start offset into the buffer
  • byteLength: number — Current length in bytes
  • maxByteLength: number — Maximum capacity in bytes
  • length: number — Alias for byteLength

Methods

  • reset(): void — Clear the slice back to empty state. Note: this does not return the slot to the PoolAllocator — you must call realloc(slice, 0) to free pool memory.
  • copy(target: Buffer | Slice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number — Copy data to a Buffer or Slice. Returns bytes copied.
  • compare(target: Buffer | Slice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1 — Compare with a Buffer or Slice
  • write(string: string, offset?: number, length?: number, encoding?: BufferEncoding): number — Write a string into the slice. Returns bytes written.
  • set(source: Buffer | Slice | null | undefined, offset?: number): void — Copy from a Buffer or Slice into this slice
  • at(index: number): number — Read byte at index (supports negative indexing)
  • test(expr: { test(buffer: Buffer, byteOffset: number, byteLength: number): boolean }): boolean — Test the slice against an expression object
  • toString(encoding?: BufferEncoding, start?: number, end?: number): string — Convert to string
  • toBuffer(start?: number, end?: number): Buffer — Return a Buffer view

Static

  • Slice.EMPTY_BUF: Buffer — Shared empty buffer singleton

PoolAllocator

Pre-allocates a contiguous memory pool and manages slices using power-of-2 bucketing.

new PoolAllocator(poolTotal?: number)

Creates a pool allocator. Default pool size is 128 MB.

Methods

  • realloc(slice: Slice, byteLength: number): Slice — Allocate, resize, or free a slice. Pass 0 to free.
  • isFromPool(slice: Slice | null | undefined): boolean — Check if a slice was allocated from this pool

Properties

  • size: number — Total size of all active allocations
  • stats: { size: number, padding: number, ratio: number, poolTotal: number, poolUsed: number, poolSize: number, poolCount: number } — Detailed allocation statistics

License

MIT