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

v2.0.0

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 for in-pool sizes it never allocates new backing stores (only allocations larger than the 256 KB top bucket, or made once the contiguous pool is exhausted, fall back to a standalone Buffer). It pre-allocates a large contiguous buffer and hands out regions through lightweight PoolSlice handles using power-of-2 bucketing. When a slice is freed, its slot is recycled. When an existing slice is resized within the same bucket, no data moves and no new handle is created — only its allocator-owned metadata changes. 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 = pool.realloc(64) // returns a PoolSlice
s.write('hello')
pool.realloc(s, 128) // grow — moves to the 128-byte bucket (contents not preserved)
pool.realloc(s, 100) // shrink within the bucket — in-place, just a field update
pool.realloc(s, 0) // free — slot is recycled

Benchmarks

The tables are a historical Apple M3 Pro, Node.js v25.6.1 snapshot of the pre-PoolSlice implementation, retained for directional context rather than presented as current measurements. The project targets Node.js 26; run node packages/slice/src/index.bench.ts from the repository root for current results on your hardware. In the Allocation groups, every PoolAllocator iteration performs a real alloc + free pair (a bare repeated realloc(slice, N) would hit the same-size early return and measure a no-op).

Allocation (alloc + free per iteration)

| Operation | Buffer.allocUnsafe | Buffer.allocUnsafeSlow | PoolAllocator | Speedup | | --------------------- | -------------------: | -----------------------: | --------------: | ------: | | alloc/free 64 bytes | 28.87 ns | 31.08 ns | 14.05 ns | 2.1x | | alloc/free 256 bytes | 38.22 ns | 172.16 ns | 12.97 ns | 2.9x | | alloc/free 1024 bytes | 61.32 ns | 226.35 ns | 13.16 ns | 4.7x | | alloc/free 4096 bytes | 285.70 ns | 272.15 ns | 13.21 ns | 20.6x |

Allocation (GC)

| Operation | Buffer.allocUnsafe | Buffer.allocUnsafeSlow | PoolAllocator | Speedup | | --------------------- | -------------------: | -----------------------: | --------------: | ------: | | alloc/free 64 bytes | 204.54 ns | 107.27 ns | 17.60 ns | 6.1x | | alloc/free 256 bytes | 198.66 ns | 324.21 ns | 17.58 ns | 11.3x | | alloc/free 4096 bytes | 378.27 ns | 360.77 ns | 17.28 ns | 20.9x |

In that implementation, the GC-pressure advantage reached ~21x because two-argument realloc reused both a pre-allocated slot and its existing handle. The current API preserves that reuse through PoolAllocator.realloc(existingPoolSlice, size); its single-argument form intentionally creates a fresh PoolSlice. Historical speedup is computed against the faster of the two Buffer variants.

Slice creation vs Buffer.subarray

| Operation | Buffer.subarray | Slice | Speedup | | ---------------------- | ----------------: | -----------: | ------: | | subarray 64 bytes | 29.32 ns | 4.13 ns | 7.1x | | subarray 1024 bytes | 28.60 ns | 4.11 ns | 7.0x | | subarray 64 bytes (GC) | 90.84 ns | 80.43 ns | 1.1x |

Combined operations

Buffer is the faster of Buffer.allocUnsafe/Buffer.allocUnsafeSlow per row.

| Operation | Buffer | PoolAllocator | Speedup | | --------------------------------------- | --------: | --------------: | ------: | | fresh handle + alloc/free 64 bytes | 25.54 ns | 15.44 ns | 1.7x | | fresh handle + alloc/free 64 bytes (GC) | 116.92 ns | 61.42 ns | 1.9x | | fresh handle + alloc/free 256 bytes | 29.62 ns | 15.38 ns | 1.9x | | realloc churn (64 → 128 → 64) | 48.76 ns | 17.30 ns | 2.8x | | realloc in-place (grow within bucket) | 48.39 ns | 6.84 ns | 7.1x | | 10 concurrent allocs then free | 307.90 ns | 176.16 ns | 1.7x | | 10 concurrent allocs then free (GC) | 470.07 ns | 372.63 ns | 1.3x | | 10 concurrent allocs then free ×2 | 635.30 ns | 345.13 ns | 1.8x |

API

SliceLike

The shared read-only structural interface implemented by both Slice and PoolSlice:

interface SliceLike {
  readonly buffer: Buffer
  readonly byteOffset: number
  readonly byteLength: number
}

The interface makes view metadata read-only to generic consumers. It is shallow: callers can still read and write bytes through buffer.

Slice

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

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

Creates a new slice. All parameters are optional — defaults to an empty slice. byteLength defaults to the remaining bytes (buffer.byteLength - byteOffset), matching Slice.fromBuffer and typed-array conventions. The constructor does no validation; pass only consistent values, or use Slice.fromBuffer for a checked construction.

Slice.fromBuffer(buffer: Buffer, byteOffset?: number, byteLength?: number): Slice

Validated factory for a zero-copy window over an existing Buffer. byteOffset defaults to 0 and byteLength to the remaining bytes (buffer.byteLength - byteOffset). Throws TypeError if buffer is not a Buffer, and RangeError if byteOffset/byteLength are negative, non-integer, or byteOffset + byteLength exceeds the buffer. Prefer this over the bare constructor when the inputs are untrusted.

Note: In earlier versions this method was named Slice.from. Use Slice.fromBuffer when you need its explicit bounds validation or a byte window within an existing Buffer.

Slice.fromString(string: string, encoding?: BufferEncoding): Slice

Allocates a fresh Buffer from string (via Buffer.from(string, encoding)) and wraps it. encoding defaults to 'utf8'.

Slice.from(...): Slice

Accepts the same input forms as Buffer.from, while preserving Slice's view semantics: direct Buffer, typed-array, ArrayBuffer, and SharedArrayBuffer inputs remain zero-copy views of their original byte window. Plain arrays and strings have no backing byte buffer to view, so those forms allocate a new Buffer. The ArrayBuffer forms accept optional byteOffset and length arguments.

Properties

  • buffer: Buffer — The underlying Buffer (mutable)
  • byteOffset: number — Start offset into the buffer (mutable)
  • byteLength: number — Current length in bytes (mutable)
  • length: number — Alias for byteLength

Methods

  • reset(): void — Clear the manually managed slice back to empty state.
  • copy(target: Uint8Array | Slice | PoolSlice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number — Copy data to a Uint8Array/Buffer, Slice, or PoolSlice. Returns bytes copied.
  • compare(target: Uint8Array | Slice | PoolSlice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1 — Compare with a Uint8Array/Buffer, Slice, or PoolSlice.
  • write(string: string, offset?: number, length?: number, encoding?: BufferEncoding): number — Write a string into the slice. Returns bytes written.
  • set(source: Buffer | Slice | PoolSlice | null | undefined, offset?: number): void — Copy from a Buffer, Slice, or PoolSlice into this slice. (A plain Uint8Array source is not accepted — it has no copy method.)
  • at(index: number): number — Read byte at integer 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

Validation & bounds

All offsets are relative to the slice (i.e. 0 is byteOffset). For a Slice or PoolSlice target, target offsets are relative to that slice; for a raw Buffer/Uint8Array target they are absolute (passed straight through to the underlying Buffer method).

The rule is consistent across the API:

  • Start/offset arguments are validatedset's offset, copy/compare's sourceStart/targetStart, toString/toBuffer's start, write's offset, and at's index must be in-range integers. Out-of-range or non-integer values throw RangeError. This prevents a negative offset from resolving to a position before the slice and reading/writing adjacent (pool) memory.
  • End arguments are validated, then clampedsourceEnd/targetEnd/end must be integers (NaN, fractional, and ±Infinity throw RangeError; omit the argument to mean "to the end of the slice"). In-range validation stays lenient: integer ends beyond the slice are clamped to the slice's logical length (matching Buffer's end-of-range behavior), so over-long ranges never read past the slice's end.
  • write's length is hybrid — a negative or non-integer length throws RangeError, but a too-large length is clamped to the bytes available in the slice (it behaves like an end argument, not a strict start argument).
  • For a raw Buffer/Uint8Array target, targetStart/targetEnd (and a raw compare target's range) are passed straight through to the underlying Buffer method, so they follow Buffer's own coercion/validation rather than the rules above.

Static

  • Slice.EMPTY_BUF: Buffer — Shared empty buffer singleton

PoolSlice

The allocator-owned sibling of Slice. PoolAllocator.realloc(byteLength) creates it; callers resize or free the same handle by passing it back to the allocator. It provides the same buffer-view methods as Slice, but deliberately has no reset() method. It is a separate class, so an allocated handle is a PoolSlice, not an instanceof Slice.

Properties

  • readonly buffer: Buffer — The current backing buffer
  • readonly byteOffset: number — Start offset into the buffer
  • readonly byteLength: number — Current logical length
  • readonly maxByteLength: number — Allocator capacity: the power-of-2 bucket size for a pool allocation, or retained capacity for a heap fallback
  • length: number — Alias for byteLength

Only PoolAllocator changes these four metadata properties. The read-only fields and private constructor enforce that contract in TypeScript without adding per-instance ownership fields or runtime guards. They are intentionally shallow and compile-time-only: buffer bytes remain writable, and constructing or mutating a PoolSlice from plain JavaScript is unsupported.

This is a breaking allocator-handle change. To migrate:

  • Replace const slice = new Slice(); pool.realloc(slice, size) with const slice = pool.realloc(size).
  • Change stored allocator-handle and parameter types from Slice to PoolSlice.
  • Use SliceLike when code only needs a shared read-only view of either class.
  • Remove manual Slice.maxByteLength usage; capacity exists only on PoolSlice.

PoolAllocator

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

new PoolAllocator(poolTotalOrBuffer?: number | Buffer | ArrayBufferView | ArrayBuffer | SharedArrayBuffer)

Creates a pool allocator. Pass a byte size to allocate a fresh backing buffer (default 128 MB, must be a non-negative integer), or pass an existing Buffer/ArrayBufferView/ArrayBuffer/SharedArrayBuffer to back the pool with caller-provided memory. Infinity means unbounded: no pool is pre-allocated, every allocation falls back to a standalone Buffer, and size stays 0 — so size-based pruning never triggers. NaN, negative, and fractional sizes throw RangeError.

Single-owner. The allocator's bookkeeping lives in the instance, not in the backing buffer. When you supply your own buffer, that buffer must be owned exclusively by this allocator: do not share it with a second PoolAllocator, and (for a SharedArrayBuffer) do not allocate from more than one thread — the metadata is not shared or atomic, so doing either can produce overlapping allocations.

Methods

  • realloc(byteLength: number): PoolSlice — Allocate and return a fresh allocator-owned handle.
  • realloc(slice: PoolSlice, byteLength: number): PoolSlice — Resize a handle returned by this allocator, or free it by passing 0. Contents are not preservedrealloc has malloc semantics, not C realloc semantics; after a resize the bytes are undefined (a same-bucket or in-place resize happens to keep them in place, but do not rely on it). Do not pass a handle from another allocator. Calling realloc(slice, 0) twice on the same object is a harmless no-op.
  • isFromPool(slice: SliceLike | null | undefined): boolean — Check whether a slice-like view's buffer is this pool's backing buffer. This is an identity check, not an ownership proof.

Allocations larger than 256 KB (the largest bucket), or made when the contiguous pool is exhausted, fall back to a fresh standalone Buffer (isFromPool returns false) and are excluded from size/stats. Resizing a standalone PoolSlice reuses its existing backing store when the request fits and no destination pool slot is available. Capacity is retained only up to the power-of-2 ceiling of the request (< 2× waste), so size churn within retained capacity does not reallocate; a wider shrink releases the oversized buffer. A free or uncarved destination pool slot takes priority, so an eligible standalone slice rejoins the pool when space becomes available.

Properties

  • size: number — Total reserved bytes of all active pool allocations (sum of bucket sizes; equals stats.poolSize).
  • stats — Detailed allocation statistics:
    • size — same as the size getter (active pool bytes, including power-of-2 padding).
    • padding — bytes lost to power-of-2 rounding across active pool slices.
    • ratiosize / (size - padding); 1 when there is no padding.
    • poolTotal — capacity of the backing buffer in bytes.
    • poolUsed — bump-pointer high-water mark; monotonic, never decreases.
    • poolSize — active pool bytes (same as size).
    • poolCount — number of distinct slots ever bump-allocated (monotonic high-water count, not a live count).
    • buckets — per power-of-2 bucket: { free, used, size }.

License

MIT