
Security News
The Code You Didn't Write Is Still Yours to Defend
AI agents are pulling packages into environments no scanner is watching, creating exposure before security teams can see it.
@nxtedition/slice
Advanced tools
A high-performance buffer slice and pool allocator for Node.js.
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), 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.
npm install @nxtedition/slice
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 — 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
Measured on Apple M3 Pro, Node.js v25.6.1. 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).
| 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 |
| 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 |
Under GC pressure the advantage grows — up to ~21x — because PoolAllocator reuses slots from a pre-allocated buffer and never creates objects for V8 to trace. Speedup is computed against the faster of the two Buffer variants.
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 |
Buffer is the faster of Buffer.allocUnsafe/Buffer.allocUnsafeSlow per row.
| Operation | Buffer | PoolAllocator | Speedup |
|---|---|---|---|
| new Slice + alloc/free 64 bytes | 25.54 ns | 15.44 ns | 1.7x |
| new Slice + alloc/free 64 bytes (GC) | 116.92 ns | 61.42 ns | 1.9x |
| new Slice + 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 |
SliceA 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. byteLength defaults to the remaining bytes (buffer.byteLength - byteOffset), matching Slice.from and typed-array conventions. The constructor does no validation; pass only consistent values, or use Slice.from for a checked construction.
Slice.from(buffer: Buffer, byteOffset?: number, byteLength?: number): SliceValidated factory for wrapping 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.
buffer: Buffer — The underlying BufferbyteOffset: number — Start offset into the bufferbyteLength: number — Current length in bytesmaxByteLength: number — For a pool-allocated slice, the power-of-2 bucket capacity (always >= byteLength). For a heap-fallback slice it tracks the allocation's capacity (the high-water byteLength), which realloc uses to resize large slices in place. For a manually-constructed slice it defaults to byteLength and no Slice method bounds reads or writes against it (those are validated/clamped against byteLength) — but realloc treats any non-pool slice's maxByteLength as its capacity if such a slice is passed to it.length: number — Alias for byteLengthreset(): void — Clear the slice back to empty state. Note: this does not return the slot to the PoolAllocator. To free pool memory call realloc(slice, 0) — and do not reset() first: reset() detaches the slice from the pool buffer, so a subsequent realloc(slice, 0) can no longer locate the slot and the slot leaks.copy(target: Uint8Array | Slice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number — Copy data to a Uint8Array/Buffer or Slice. Returns bytes copied.compare(target: Uint8Array | Slice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1 — Compare with a Uint8Array/Buffer or Slicewrite(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. (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 objecttoString(encoding?: BufferEncoding, start?: number, end?: number): string — Convert to stringtoBuffer(start?: number, end?: number): Buffer — Return a Buffer viewAll offsets are relative to the slice (i.e. 0 is byteOffset). For a Slice 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:
set'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.sourceEnd/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).Buffer/Uint8Array target, targetStart/targetEnd (and a non-Slice 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.Slice.EMPTY_BUF: Buffer — Shared empty buffer singletonPoolAllocatorPre-allocates a contiguous memory pool and manages slices 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 build your own
Sliceviews over it, do not share it with a secondPoolAllocator, and (for aSharedArrayBuffer) do not allocate from more than one thread — the metadata is not shared or atomic, so doing any of these silently produces overlapping allocations.
realloc(byteLength: number): Slice — Allocate a fresh slice.realloc(slice: Slice, byteLength: number): Slice — Resize a slice, or free it by passing 0. Contents are not preserved — realloc 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). Only call realloc with a slice that belongs to this allocator (or a fresh/empty Slice); passing a slice from another pool over the same backing buffer, or freeing the same pool slot through two aliasing Slice objects, throws Invalid pool state when the corruption is detectable (a bucket's live count would go negative) — but do not rely on detection: an aliasing free while other slices remain live in the same bucket silently corrupts the accounting. (Calling realloc(slice, 0) twice on the same object is a harmless no-op — the first free empties the slice, so the second returns immediately.)isFromPool(slice: Slice | null | undefined): boolean — Check if a slice's buffer is this pool's backing buffer. Note this is an identity check; it returns true for any slice over the same buffer, not only ones this allocator handed out.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 such a slice while the request stays above 256 KB reuses the existing backing store in place when it fits — capacity is retained up to the power-of-2 ceiling of the request (< 2× waste), so size churn within the retained capacity does not reallocate (churn spanning a wider than 2× band still reallocates); shrinking below that bound, or to a pool-eligible size, allocates anew (rejoining the pool when possible). Note the in-place path keeps whatever backing buffer the slice already has — including a hand-constructed one — so do not use realloc to detach a slice from caller-shared memory.
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.ratio — size / (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 }.MIT
FAQs
A high-performance buffer slice and pool allocator for Node.js.
The npm package @nxtedition/slice receives a total of 108 weekly downloads. As such, @nxtedition/slice popularity was classified as not popular.
We found that @nxtedition/slice demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 12 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
AI agents are pulling packages into environments no scanner is watching, creating exposure before security teams can see it.

Security News
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.