@arkv/rng
v0.8.2
Published
Fastest, zero-dependency, pseudo-RNG in node and browser environments, with support for cryptographic randomness and seedable PRNG.
Maintainers
Readme
@arkv/rng
Cryptographically correct uniform distributions!
Fastest, seedable, zero dependency, isomorphic pseudo-random number generator powered by Rust + WebAssembly. Works identically in Node.js, Bun, and the browser — no native compilation required.
Five PRNG algorithms are available (pcg64, xoroshiro128+, xorshift128+, mersenne, lcg32), all seedable with a number, bigint, or string. String seeds are hashed to a u64 via FNV-1a 64-bit inside the WebAssembly layer — no JavaScript hashing.
@arkv/rng is heavily optimized for zero-overhead boundary crossing. Calling .int() repeatedly inside a JavaScript loop incurs microscopic overhead as execution passes between JS and WebAssembly.
To bypass this for large datasets, use the batch methods: .ints(N), .floats(N), .ranges(min, max, N), and .shuffle(array). These methods compute the entire sequence natively in Rust and return the results as a single typed array, eliminating per-value boundary crossings entirely.
Table of Contents
Install
bun add @arkv/rng
# or
npm install @arkv/rngUsage
Basic
const rng = new Rng(); // system entropy
rng.int(); // u32 in [0, 2^32)
rng.float(); // float in [0, 1)
rng.range(1, 10); // integer in [1, 10)
rng.bool(); // boolean (50/50)
rng.free(); // release Wasm memorySeeded (deterministic)
Pass any number, bigint, or string seed for reproducible sequences:
// number seed
const rng1 = new Rng(12345);
console.log(rng1.int()); // always the same value
// bigint seed
const rng2 = new Rng(12345n);
console.log(rng2.int()); // always the same value
// string seed — hashed to u64 via FNV-1a inside WebAssembly
const rng3 = new Rng('hello.');
console.log(rng3.int()); // always the same value
console.log(rng3.int()); // always the next value in the sequence
// note: 'hello.' and 'world.' produce different sequences
// note: '42' and 42 produce different sequencesChoosing an algorithm
The default algorithm is pcg64. Pass a second argument to select another:
const rng = new Rng('my seed', 'xoroshiro128+');
// Available algorithms:
// 'pcg64' — default, excellent statistical quality
// 'xoroshiro128+' — fast, 128-bit state
// 'xorshift128+' — fast, minimal state
// 'mersenne' — MT19937-64, 623-dimensional equidistribution
// 'lcg32' — simplest, highest throughputArray utilities
const rng = new Rng();
rng.ints(100); // Uint32Array of 100 random integers
rng.floats(100); // Float64Array of 100 random floats in [0, 1)
rng.ranges(1, 100, 50); // Uint32Array of 50 integers in [1, 100)
rng.pick([1, 2, 3, 4, 5]); // Returns a random element
rng.shuffle([1, 2, 3, 4, 5]); // Returns a new, randomly shuffled array
rng.bool(0.8); // Returns true 80% of the timeAPI
new Rng(seed?, algorithm?)
| Param | Type | Description |
|-------|------|-------------|
| seed | number \| bigint \| string | Optional seed for deterministic output. Omit for system entropy. String seeds are hashed via FNV-1a 64-bit in Rust. |
| algorithm | RngAlgorithm | PRNG backend. Default: 'pcg64'. |
type RngAlgorithm = 'pcg64' | 'xoroshiro128+' | 'xorshift128+' | 'mersenne' | 'lcg32';| Method | Return | Description |
|--------|--------|-------------|
| int() | number | Random u32 in [0, 2^32) |
| ints(length) | Uint32Array | Batched array of random u32 integers |
| bigInt() | bigint | Random u64 as a BigInt in [0, 2^64) |
| intStream(bufferSize?) | () => number | High-throughput buffered integer closure (default buffer: 256) |
| float() | number | Random float in [0, 1) with 53-bit precision |
| floats(length) | Float64Array | Batched array of random floats in [0, 1) |
| range(min, max) | number | Random integer in [min, max) |
| ranges(min, max, length) | Uint32Array | Batched array of random integers in [min, max) |
| bool(probability?) | boolean | Random boolean; default 0.5 |
| pick(array) | T | Random element from a non-empty array |
| shuffle(array) | T[] | New randomly ordered copy of the array |
| free() | void | Release Wasm memory |
License
Development
Prerequisites
| Tool | Install |
|------|---------|
| Bun >= 1.0 | curl -fsSL https://bun.sh/install \| bash |
| Rust stable | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh |
| wasm-pack >= 0.13 | cargo install wasm-pack |
Run the setup script to install Rust + wasm-pack in one step:
bash packages/rng/setup.shBuild
bun install
cd packages/rng
# Full build: Rust -> Wasm -> TypeScript -> dist/
bun run build
# Wasm only (run once before tests, or after changing Rust)
bun run build:wasm
# TypeScript only (after Wasm is already built)
bun run build:tsTest
# Wasm must be built first
bun run build:wasm
bun test
bun test --coverageBenchmark
Run bun run build:wasm && bun run bench to reproduce.
Compared against: seedrandom (all 7 algorithm variants), pure-rand (all 4 algorithms),
random-js (Mersenne Twister).
1 · Sequential u32 Integer (N=100,000)
| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 | 1.49 | 67,055,499 | 4.01x | | @arkv/rng · xoroshiro128+ | 0.96 | 104,698,009 | 2.57x | | @arkv/rng · xorshift128+ | 1.31 | 76,163,783 | 3.53x | | @arkv/rng · mersenne | 2.01 | 49,729,051 | 5.41x | | @arkv/rng · lcg32 | 1.15 | 86,741,629 | 3.10x | | seedrandom · default/ARC4 | 2.77 | 36,061,207 | 7.46x | | seedrandom · alea | 0.61 | 165,099,596 | 1.63x | | seedrandom · xor128 | 0.37 | 268,945,170 | fastest | | seedrandom · tychei | 1.04 | 95,853,566 | 2.81x | | seedrandom · xorwow | 2.00 | 50,008,351 | 5.38x | | seedrandom · xor4096 | 2.13 | 46,994,093 | 5.72x | | seedrandom · xorshift7 | 0.50 | 199,527,519 | 1.35x | | pure-rand · xoroshiro128+ (uniform) | 1.34 | 74,835,120 | 3.59x | | pure-rand · xorshift128+ (uniform) | 1.40 | 71,432,092 | 3.77x | | pure-rand · mersenne (uniform) | 1.24 | 80,884,619 | 3.33x | | pure-rand · congruential32 (uniform) | 0.85 | 117,371,305 | 2.29x | | random-js · MersenneTwister | 2.49 | 40,179,361 | 6.69x |
2 · Batched u32 Array (100 k elements) (N=100,000)
| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 [native batch] | 0.51 | 195,433,119 | 5.16x | | @arkv/rng · xoroshiro128+ [native batch] | 0.26 | 383,756,361 | 2.63x | | @arkv/rng · xorshift128+ [native batch] | 0.58 | 173,098,727 | 5.83x | | @arkv/rng · mersenne [native batch] | 0.93 | 107,157,362 | 9.41x | | @arkv/rng · lcg32 [native batch] | 0.10 | 1,008,766,178 | fastest | | pure-rand · xoroshiro128+ loop | 2.14 | 46,623,568 | 21.64x | | pure-rand · xorshift128+ loop | 2.42 | 41,285,805 | 24.43x | | pure-rand · mersenne loop | 2.46 | 40,607,983 | 24.84x | | pure-rand · congruential32 loop | 2.24 | 44,656,254 | 22.59x | | random-js loop | 1.63 | 61,358,238 | 16.44x |
3 · Float [0, 1) — 53-bit precision
`@arkv/rng` and `pure-rand` generate high-resolution floats with full **53-bit precision** (IEEE 754 standard). `seedrandom` generates lower-resolution floats with only **32-bit precision**. This quality difference explains pure-rand's slower single-call numbers — it rolls two 32-bit integers per float, whereas seedrandom rolls just one. (N=100,000)| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 [batch] | 0.55 | 182,867,174 | 3.01x | | @arkv/rng · xoroshiro128+ [batch] | 0.87 | 115,407,209 | 4.77x | | @arkv/rng · xorshift128+ [batch] | 0.30 | 330,063,504 | 1.67x | | @arkv/rng · mersenne [batch] | 0.87 | 115,060,631 | 4.78x | | @arkv/rng · lcg32 [batch] | 0.18 | 549,958,203 | fastest | | @arkv/rng · pcg64 [single] | 0.86 | 116,756,004 | 4.71x | | seedrandom · default/ARC4 | 2.77 | 36,127,925 | 15.22x | | seedrandom · alea | 0.65 | 154,310,272 | 3.56x | | seedrandom · xor128 | 0.37 | 267,106,145 | 2.06x | | seedrandom · tychei | 0.58 | 173,306,020 | 3.17x | | seedrandom · xorwow | 0.86 | 116,921,185 | 4.70x | | seedrandom · xor4096 | 0.86 | 116,428,396 | 4.72x | | seedrandom · xorshift7 | 0.50 | 198,261,642 | 2.77x | | pure-rand · xoroshiro128+ | 1.45 | 69,015,875 | 7.97x | | pure-rand · xorshift128+ | 1.53 | 65,271,510 | 8.43x | | pure-rand · mersenne | 2.16 | 46,383,523 | 11.86x | | pure-rand · congruential32 | 1.54 | 65,118,030 | 8.45x | | random-js · Random.real(0, 1) | 1.95 | 51,362,493 | 10.71x |
4 · Bounded Range [1, 1000) — uniform distribution
`@arkv/rng` uses **unbiased rejection sampling** (via the `rand` crate), which guarantees a perfectly uniform distribution. `seedrandom + Math.floor()` uses biased float multiplication — faster but mathematically incorrect (modulo bias). `pure-rand` also uses unbiased sampling, explaining its slower numbers. `@arkv/rng` produces cryptographically correct uniform integers faster than `seedrandom` produces biased ones. (N=100,000)| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 ranges [batch] | 0.68 | 146,217,001 | 3.06x | | @arkv/rng · xoroshiro128+ ranges [batch] | 0.41 | 246,783,181 | 1.81x | | @arkv/rng · xorshift128+ ranges [batch] | 0.41 | 246,673,606 | 1.82x | | @arkv/rng · mersenne ranges [batch] | 0.53 | 187,964,625 | 2.38x | | @arkv/rng · lcg32 ranges [batch] | 0.22 | 447,906,262 | fastest | | @arkv/rng · pcg64 range() [single] | 0.99 | 100,858,508 | 4.44x | | seedrandom · default/ARC4 + floor | 3.02 | 33,057,917 | 13.55x | | seedrandom · alea + floor | 0.68 | 147,248,081 | 3.04x | | seedrandom · xor128 + floor | 0.44 | 229,293,112 | 1.95x | | seedrandom · tychei + floor | 1.06 | 94,392,251 | 4.75x | | seedrandom · xorwow + floor | 1.89 | 52,905,154 | 8.47x | | seedrandom · xor4096 + floor | 2.09 | 47,753,964 | 9.38x | | seedrandom · xorshift7 + floor | 0.57 | 174,900,088 | 2.56x | | pure-rand · xoroshiro128+ uniformInt | 1.03 | 96,852,488 | 4.62x | | pure-rand · xorshift128+ uniformInt | 1.06 | 94,179,872 | 4.76x | | pure-rand · mersenne uniformInt | 1.28 | 77,834,337 | 5.75x | | pure-rand · congruential32 uniformInt | 0.90 | 111,187,954 | 4.03x | | random-js · Random.integer(1, 999) | 8.95 | 11,170,848 | 40.10x |
5 · Array Shuffle (100 k elements) (N=100,000)
| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 shuffle() | 2.36 | 42,350,827 | 1.65x | | @arkv/rng · xoroshiro128+ shuffle() | 1.44 | 69,498,885 | 1.01x | | @arkv/rng · xorshift128+ shuffle() | 1.94 | 51,463,731 | 1.36x | | @arkv/rng · mersenne shuffle() | 2.12 | 47,137,415 | 1.48x | | @arkv/rng · lcg32 shuffle() | 1.43 | 69,870,900 | fastest | | seedrandom · default/ARC4 Fisher-Yates | 7.88 | 12,687,565 | 5.51x | | seedrandom · alea Fisher-Yates | 5.66 | 17,656,689 | 3.96x | | seedrandom · xor128 Fisher-Yates | 6.26 | 15,973,675 | 4.37x | | seedrandom · tychei Fisher-Yates | 8.45 | 11,833,748 | 5.90x | | seedrandom · xorwow Fisher-Yates | 7.85 | 12,738,832 | 5.48x | | seedrandom · xor4096 Fisher-Yates | 8.84 | 11,311,998 | 6.18x | | seedrandom · xorshift7 Fisher-Yates | 4.58 | 21,827,694 | 3.20x | | pure-rand · xoroshiro128+ Fisher-Yates | 8.81 | 11,349,010 | 6.16x | | pure-rand · xorshift128+ Fisher-Yates | 8.01 | 12,484,014 | 5.60x | | pure-rand · mersenne Fisher-Yates | 8.67 | 11,536,395 | 6.06x | | pure-rand · congruential32 Fisher-Yates | 8.14 | 12,279,030 | 5.69x | | random-js · Random.shuffle() [in-place] | 3.07 | 32,554,574 | 2.15x |
6 · String-seeded Float [0, 1) (N=100,000)
| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 [string seed] | 0.91 | 110,284,346 | 2.10x | | @arkv/rng · xoroshiro128+ [string seed] | 0.85 | 118,259,644 | 1.96x | | @arkv/rng · xorshift128+ [string seed] | 1.03 | 97,139,438 | 2.38x | | @arkv/rng · mersenne [string seed] | 1.45 | 68,875,646 | 3.36x | | @arkv/rng · lcg32 [string seed] | 1.68 | 59,598,556 | 3.88x | | seedrandom · default/ARC4 [string seed] | 2.86 | 34,948,502 | 6.62x | | seedrandom · alea [string seed] | 0.72 | 138,649,141 | 1.67x | | seedrandom · xor128 [string seed] | 0.43 | 231,398,992 | fastest | | seedrandom · tychei [string seed] | 0.61 | 163,638,000 | 1.41x | | seedrandom · xorwow [string seed] | 0.80 | 124,589,478 | 1.86x | | seedrandom · xor4096 [string seed] | 0.85 | 117,815,188 | 1.96x | | seedrandom · xorshift7 [string seed] | 0.51 | 197,583,944 | 1.17x |
7 · intStream() — Buffered Single Integer (N=100,000)
| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 intStream() | 1.35 | 73,939,832 | 3.57x | | @arkv/rng · xoroshiro128+ intStream() | 1.12 | 89,029,833 | 2.97x | | @arkv/rng · lcg32 intStream() | 0.87 | 115,133,492 | 2.29x | | seedrandom · default/ARC4 | 2.82 | 35,489,523 | 7.44x | | seedrandom · alea | 0.66 | 150,789,761 | 1.75x | | seedrandom · xor128 | 0.38 | 264,212,659 | fastest | | seedrandom · tychei | 1.05 | 95,109,652 | 2.78x | | seedrandom · xorwow | 2.01 | 49,842,572 | 5.30x | | seedrandom · xor4096 | 2.14 | 46,680,176 | 5.66x | | seedrandom · xorshift7 | 0.53 | 188,973,759 | 1.40x |
8 · Native 64-bit BigInt
`@arkv/rng` generates a 64-bit integer natively in Rust in a single CPU operation. Pure-JS libraries must roll two 32-bit values and stitch them via BigInt arithmetic — consuming twice the RNG calls and adding JS BigInt overhead. (N=100,000)| Library | ms | ops/sec | vs fastest | |:--------|---:|-------:|----------:| | @arkv/rng · pcg64 bigInt() | 2.67 | 37,501,064 | fastest | | @arkv/rng · xoroshiro128+ bigInt() | 5.13 | 19,483,333 | 1.92x | | @arkv/rng · xorshift128+ bigInt() | 2.99 | 33,476,120 | 1.12x | | @arkv/rng · mersenne bigInt() | 3.20 | 31,266,943 | 1.20x | | @arkv/rng · lcg32 bigInt() | 2.93 | 34,082,896 | 1.10x | | seedrandom · default/ARC4 (2×32-bit + BigInt) | 11.86 | 8,431,821 | 4.45x | | seedrandom · alea (2×32-bit + BigInt) | 5.39 | 18,536,667 | 2.02x | | seedrandom · xor128 (2×32-bit + BigInt) | 9.05 | 11,052,370 | 3.39x | | seedrandom · tychei (2×32-bit + BigInt) | 12.19 | 8,204,463 | 4.57x | | seedrandom · xorwow (2×32-bit + BigInt) | 11.75 | 8,508,427 | 4.41x | | seedrandom · xor4096 (2×32-bit + BigInt) | 8.62 | 11,597,512 | 3.23x | | seedrandom · xorshift7 (2×32-bit + BigInt) | 12.04 | 8,307,907 | 4.51x |
