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

@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.

Readme

qs-fast

npm version license

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-fast

qs 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 custom depth, charset, a custom encoder/decoder, …) and the call is served entirely by qs. Output is byte-for-byte qs.
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 from qs unchanged.
  • default export { parse, stringify, formats } — mirrors qs'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 to qs the 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 what qs would 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 to qs's own encode, so multi-byte/unicode output is identical. Buffers and cycles bail to qs.
  • Fallback gate — any non-default options object routes the whole call to qs before 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 DhandhaGitHub

License

MIT