@libs-jd/qs-fast
v0.1.0
Published
Drop-in faster qs — an optimized fast path for parse/stringify on the common default-options case (~2.6x parse, ~2.0x stringify) with automatic, transparent fallback to qs for exotic options. Output is byte-for-byte identical to qs.
Maintainers
Readme
qs-fast
A drop-in, faster qs — same API, same output, less work.
qs.parse / qs.stringify do a lot of defensive work (deep-clone merges, a per-key side channel, per-node option plumbing) that the overwhelmingly common case — default options — doesn't need. qs-fast handles that common case on an allocation-light fast path, and transparently falls back to real qs for any non-default option or exotic shape it doesn't fully replicate.
Because the fast path only owns shapes it reproduces exactly and defers everything else to qs, output is always identical — verified element-for-element against qs across 142 cases (0 mismatches). You never trade correctness for speed.
| Payload (node v24, Apple Silicon, best-of-15) | qs | qs-fast | Speedup |
| --- | --- | --- | --- |
| parse small flat | 350 ms | 115 ms | 3.0x |
| parse medium nested | 721 ms | 202 ms | 3.6x |
| parse array-heavy | 864 ms | 490 ms | 1.8x |
| stringify small flat | 251 ms | 37 ms | 6.8x |
| stringify medium nested | 394 ms | 159 ms | 2.5x |
| stringify array-heavy | 653 ms | 226 ms | 2.9x |
Roughly ~2.6x faster parse and ~2.0x+ faster stringify on typical query strings. (Micro-benchmark x-factors shift with engine and machine; run it yourself below.)
Reproduce with bun run bench/bench.ts.
Install
npm install @libs-jd/qs-fastqs is a dependency and comes along automatically — the fast path falls back to it.
Usage
Exactly like qs:
import qs from "@libs-jd/qs-fast";
// or: import { parse, stringify } from "@libs-jd/qs-fast";
qs.parse("a[b][c]=d&list[]=1&list[]=2&q=hello%20world");
// { a: { b: { c: "d" } }, list: ["1", "2"], q: "hello world" }
qs.stringify({ filter: { status: "active", tags: ["a", "b"] } });
// "filter%5Bstatus%5D=active&filter%5Btags%5D%5B0%5D=a&filter%5Btags%5D%5B1%5D=b"Drop it in wherever you use qs — same call signatures, same results:
- import qs from "qs";
+ import qs from "@libs-jd/qs-fast";Both ESM and CommonJS are shipped:
const qs = require("@libs-jd/qs-fast");Identical output — guaranteed
The whole point of a drop-in is that nothing downstream changes. qs-fast guarantees this two ways:
- The fast path only claims shapes it reproduces exactly. Anything it isn't 100% sure it matches — deep nesting past the default depth, array-index overflow, prototype-ish keys, buffers, cyclic objects — it hands to
qs. - Exotic options transparently fall back to
qs. Pass anything other than the defaults (allowDots,comma,arrayFormat, a customdepth,charset, a customencoder/decoder, …) and the call is served entirely byqs. Output is byte-for-byteqs.
import { parse, stringify } from "@libs-jd/qs-fast";
import qs from "qs";
// default options -> fast path, identical to qs
parse("a[b]=c"); // deep-equals qs.parse("a[b]=c")
// exotic option -> falls back to qs, still identical
parse("a.b.c=d", { allowDots: true }); // === qs.parse("a.b.c=d", { allowDots: true })
stringify({ a: [1, 2] }, { arrayFormat: "comma" });
// === qs.stringify(..., { arrayFormat: "comma" })Verified against qs across a 142-case corpus (flat, nested, indexed and bracket arrays, percent/plus/unicode encoding, empty values, duplicate keys, deep nesting, mixed) with 0 mismatches. The fast path covered ~96% of parse cases and 100% of stringify cases; the rest fell back to qs and matched.
API
Same surface as qs:
parse(str, options?)— parse a query string into an object.stringify(obj, options?)— serialize an object into a query string.formats— re-exported fromqsunchanged.- default export
{ parse, stringify, formats }— mirrorsqs's default export.
Two extras, handy for verifying coverage (not part of qs):
stats—{ parseFast, parseFallback, stringifyFast, stringifyFallback }counters.resetStats()— zero them.
How it works
parse— one pass to dedupe/combine raw pairs, one pass to build each key's nested value and merge, then a single compaction of arrays. No deep-clone merge, no per-key side object. It bails toqsthe moment a key hits a shape it doesn't own (depth > 5, array index ≥ 20,__proto__, unbalanced brackets, object input), so the result is always whatqswould produce.stringify— recursive serialize straight into a string array. Values whose every character is RFC3986-safe are emitted unchanged (no allocation); anything needing real percent-encoding defers toqs's ownencode, so multi-byte/unicode output is identical. Buffers and cycles bail toqs.- Fallback gate — any non-default options object routes the whole call to
qsbefore the fast path even runs.
About
Contributions welcome — please open an issue or PR. If this shaved time off your request handling, a ⭐ helps others find it.
Related
- qs — the query-string library this wraps and falls back to
- @libs-jd/pdf-lib-bulk — bulk PDF generation for pdf-lib
Author
Jeet Dhandha — GitHub
