├── .npmrc ├── .github ├── funding.yml ├── dependabot.yml └── workflows │ ├── linter.yml │ ├── coverage.yml │ ├── test.yml │ └── typescript-interop.yml ├── .gitignore ├── .editorconfig ├── .npmignore ├── lib ├── index.js ├── index.d.ts ├── stringify.js ├── internals │ └── querystring.js └── parse.js ├── biome.json ├── benchmark ├── import.mjs ├── stringify.mjs └── parse.mjs ├── LICENSE ├── package.json ├── scripts └── create-ts-interop-test.js ├── test ├── parse.test.ts ├── stringify.test.ts └── node.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [anonrig] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | package-lock.json 4 | test_interop 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | package-lock.json 3 | coverage 4 | test 5 | benchmark 6 | .idea 7 | .github 8 | rome.json 9 | scripts 10 | test_interop 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const parse = require("./parse"); 4 | const stringify = require("./stringify"); 5 | 6 | const fastQuerystring = { 7 | parse, 8 | stringify, 9 | }; 10 | 11 | /** 12 | * Enable TS and JS support 13 | * 14 | * - `const qs = require('fast-querystring')` 15 | * - `import qs from 'fast-querystring'` 16 | */ 17 | module.exports = fastQuerystring; 18 | module.exports.default = fastQuerystring; 19 | module.exports.parse = parse; 20 | module.exports.stringify = stringify; 21 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "indentStyle": "space", 6 | "indentWidth": 2 7 | }, 8 | "linter": { 9 | "enabled": true, 10 | "rules": { 11 | "complexity": { 12 | "all": false 13 | }, 14 | "style": { 15 | "all": false 16 | }, 17 | "suspicious": { 18 | "noRedundantUseStrict": "off" 19 | } 20 | } 21 | }, 22 | "organizeImports": { 23 | "enabled": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: linter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # This allows a subsequently queued workflow run to interrupt previous runs 12 | concurrency: 13 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Setup Biome 25 | uses: biomejs/setup-biome@v2 26 | - name: Run Biome 27 | run: biome ci . 28 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | type FastQueryString = { 2 | // biome-ignore lint/suspicious/noExplicitAny: This is deliberate. 3 | stringify(value: Record): string; 4 | // biome-ignore lint/suspicious/noExplicitAny: This is deliberate. 5 | parse(value: string): Record; 6 | }; 7 | 8 | declare namespace fastQueryString { 9 | // biome-ignore lint/suspicious/noExplicitAny: This is deliberate. 10 | export function stringify(value: Record): string; 11 | // biome-ignore lint/suspicious/noExplicitAny: This is deliberate. 12 | export function parse(value: string): Record; 13 | 14 | const fqs: FastQueryString; 15 | export { fqs as default }; 16 | } 17 | 18 | export = fastQueryString; 19 | -------------------------------------------------------------------------------- /benchmark/import.mjs: -------------------------------------------------------------------------------- 1 | import benchmark from "cronometro"; 2 | 3 | // "node:querystring" module is omitted from this benchmark because 4 | // it will always be faster than alternatives because of V8 snapshots. 5 | await benchmark( 6 | { 7 | qs() { 8 | return import("qs"); 9 | }, 10 | "fast-querystring"() { 11 | return import("../lib/index.js"); 12 | }, 13 | "query-string"() { 14 | return import("query-string"); 15 | }, 16 | querystringify() { 17 | return import("querystringify"); 18 | }, 19 | "@aws-sdk/querystring-parser"() { 20 | return import("@aws-sdk/querystring-parser"); 21 | }, 22 | querystringparser() { 23 | return import("querystringparser"); 24 | }, 25 | }, 26 | { warmup: true }, 27 | ); 28 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # This allows a subsequently queued workflow run to interrupt previous runs 12 | concurrency: 13 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Use Node.js 27 | uses: actions/setup-node@v4 28 | 29 | - name: Install dependencies 30 | run: npm install --ignore-scripts 31 | 32 | - name: Coverage 33 | run: npm run test:coverage 34 | 35 | - name: Upload Coverage 36 | uses: codecov/codecov-action@v4 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Yagiz Nizipli 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # This allows a subsequently queued workflow run to interrupt previous runs 12 | concurrency: 13 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | strategy: 19 | matrix: 20 | node-version: [18, 20, 22] 21 | os: [macos-latest, ubuntu-latest, windows-latest] 22 | runs-on: ${{ matrix.os }} 23 | permissions: 24 | contents: read 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Use Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: Install dependencies 36 | run: npm install --ignore-scripts 37 | 38 | - name: Test (Node.js) 39 | run: npm test 40 | 41 | - name: Test (Edge Runtime) 42 | run: npm run test:environment:edge 43 | 44 | - name: Test (Browser) 45 | run: npm run test:environment:browser 46 | -------------------------------------------------------------------------------- /benchmark/stringify.mjs: -------------------------------------------------------------------------------- 1 | import native from "node:querystring"; 2 | import awsQueryStringBuilder from "@aws-sdk/querystring-builder"; 3 | import benchmark from "cronometro"; 4 | import httpQuerystringStringify from "http-querystring-stringify"; 5 | import qs from "qs"; 6 | import queryString from "query-string"; 7 | import querystringify from "querystringify"; 8 | import querystringifyQs from "querystringify-ts"; 9 | import querystringparser from "querystringparser"; 10 | import fastQueryString from "../lib/index.js"; 11 | 12 | const value = { 13 | frappucino: "muffin", 14 | goat: "scone", 15 | pond: "moose", 16 | foo: ["bar", "baz", "bal"], 17 | bool: true, 18 | bigIntKey: BigInt(100), 19 | numberKey: 256, 20 | }; 21 | 22 | await benchmark( 23 | { 24 | qs() { 25 | return qs.stringify(value); 26 | }, 27 | "fast-querystring"() { 28 | return fastQueryString.stringify(value); 29 | }, 30 | "node:querystring"() { 31 | return native.stringify(value); 32 | }, 33 | "query-string"() { 34 | return queryString.stringify(value); 35 | }, 36 | URLSearchParams() { 37 | const urlParams = new URLSearchParams(value); 38 | return urlParams.toString(); 39 | }, 40 | querystringify() { 41 | return querystringify.stringify(value); 42 | }, 43 | "http-querystring-stringify"() { 44 | return httpQuerystringStringify(value); 45 | }, 46 | "@aws-sdk/querystring-builder"() { 47 | return awsQueryStringBuilder.buildQueryString(value); 48 | }, 49 | querystringparser() { 50 | return querystringparser.stringify(value); 51 | }, 52 | "querystringify-ts"() { 53 | return querystringifyQs.stringify(value); 54 | }, 55 | }, 56 | { warmup: true }, 57 | ); 58 | -------------------------------------------------------------------------------- /benchmark/parse.mjs: -------------------------------------------------------------------------------- 1 | import native from "node:querystring"; 2 | import awsQueryStringParser from "@aws-sdk/querystring-parser"; 3 | import benchmark from "cronometro"; 4 | import qs from "qs"; 5 | import queryString from "query-string"; 6 | import querystringify from "querystringify"; 7 | import querystringparser from "querystringparser"; 8 | import fastQueryString from "../lib/index.js"; 9 | 10 | const input = "frappucino=muffin&goat=scone&pond=moose&foo=bar&foo=baz"; 11 | 12 | await benchmark( 13 | { 14 | qs() { 15 | return qs.parse(input); 16 | }, 17 | "fast-querystring"() { 18 | return fastQueryString.parse(input); 19 | }, 20 | "node:querystring"() { 21 | return native.parse(input); 22 | }, 23 | "query-string"() { 24 | return queryString.parse(input); 25 | }, 26 | "URLSearchParams-with-Object.fromEntries"() { 27 | const urlParams = new URLSearchParams(input); 28 | return Object.fromEntries(urlParams); 29 | }, 30 | "URLSearchParams-with-construct"() { 31 | const u = new URLSearchParams(input); 32 | const data = {}; 33 | for (const [key, value] of u.entries()) { 34 | if (Array.isArray(data[key])) { 35 | data[key].push(value); 36 | } else if (data[key]) { 37 | data[key] = [].concat(data[key], value); 38 | } else { 39 | data[key] = value; 40 | } 41 | } 42 | return data; 43 | }, 44 | querystringify() { 45 | return querystringify.parse(input); 46 | }, 47 | "@aws-sdk/querystring-parser"() { 48 | return awsQueryStringParser.parseQueryString(input); 49 | }, 50 | querystringparser() { 51 | return querystringparser.parse(input); 52 | }, 53 | }, 54 | { warmup: true }, 55 | ); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-querystring", 3 | "version": "1.1.2", 4 | "description": "A fast alternative to legacy querystring module", 5 | "main": "./lib/index.js", 6 | "type": "commonjs", 7 | "types": "./lib/index.d.ts", 8 | "scripts": { 9 | "format": "biome check . --write", 10 | "test": "vitest", 11 | "test:environment:edge": "vitest --environment=edge-runtime", 12 | "test:environment:browser": "vitest --environment=jsdom", 13 | "test:watch": "vitest --watch", 14 | "test:coverage": "vitest --coverage", 15 | "coverage": "vitest run --coverage", 16 | "benchmark:parse": "node benchmark/parse.mjs", 17 | "benchmark:stringify": "node benchmark/stringify.mjs", 18 | "benchmark:import": "node benchmark/import.mjs" 19 | }, 20 | "keywords": ["querystring", "qs", "parser"], 21 | "author": "Yagiz Nizipli ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@aws-sdk/querystring-builder": "^3.370.0", 25 | "@aws-sdk/querystring-parser": "^3.370.0", 26 | "@biomejs/biome": "1.8.3", 27 | "@edge-runtime/vm": "^4.0.0", 28 | "@types/node": "^20.14.10", 29 | "@vitest/coverage-v8": "^2.0.1", 30 | "benchmark": "^2.1.4", 31 | "cli-select": "^1.1.2", 32 | "cronometro": "^3.0.2", 33 | "http-querystring-stringify": "^2.1.0", 34 | "jsdom": "^24.1.0", 35 | "qs": "^6.12.3", 36 | "query-string": "^9.0.0", 37 | "querystringify": "^2.2.0", 38 | "querystringify-ts": "^0.1.5", 39 | "querystringparser": "^0.1.1", 40 | "simple-git": "^3.25.0", 41 | "vitest": "^2.0.1" 42 | }, 43 | "repository": { 44 | "url": "git+https://github.com/anonrig/fast-querystring.git", 45 | "type": "git" 46 | }, 47 | "dependencies": { 48 | "fast-decode-uri-component": "^1.0.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/stringify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { encodeString } = require("./internals/querystring"); 4 | 5 | function getAsPrimitive(value) { 6 | const type = typeof value; 7 | 8 | if (type === "string") { 9 | // Length check is handled inside encodeString function 10 | return encodeString(value); 11 | } else if (type === "bigint" || type === "boolean") { 12 | return "" + value; 13 | } else if (type === "number" && Number.isFinite(value)) { 14 | return value < 1e21 ? "" + value : encodeString("" + value); 15 | } 16 | 17 | return ""; 18 | } 19 | 20 | /** 21 | * @param {Record | null>} input 23 | * @returns {string} 24 | */ 25 | function stringify(input) { 26 | let result = ""; 27 | 28 | if (input === null || typeof input !== "object") { 29 | return result; 30 | } 31 | 32 | const separator = "&"; 33 | const keys = Object.keys(input); 34 | const keyLength = keys.length; 35 | let valueLength = 0; 36 | 37 | for (let i = 0; i < keyLength; i++) { 38 | const key = keys[i]; 39 | const value = input[key]; 40 | const encodedKey = encodeString(key) + "="; 41 | 42 | if (i) { 43 | result += separator; 44 | } 45 | 46 | if (Array.isArray(value)) { 47 | valueLength = value.length; 48 | for (let j = 0; j < valueLength; j++) { 49 | if (j) { 50 | result += separator; 51 | } 52 | 53 | // Optimization: Dividing into multiple lines improves the performance. 54 | // Since v8 does not need to care about the '+' character if it was one-liner. 55 | result += encodedKey; 56 | result += getAsPrimitive(value[j]); 57 | } 58 | } else { 59 | result += encodedKey; 60 | result += getAsPrimitive(value); 61 | } 62 | } 63 | 64 | return result; 65 | } 66 | 67 | module.exports = stringify; 68 | -------------------------------------------------------------------------------- /.github/workflows/typescript-interop.yml: -------------------------------------------------------------------------------- 1 | name: Typescript Interop 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | ts-interop-test: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | package-json-type: 21 | - commonjs 22 | - module 23 | tsconfig-json-module: 24 | - CommonJS 25 | - ESNext 26 | - Node16 27 | - Nodenext 28 | tsconfig-json-module-resolution: 29 | - Node 30 | - Node16 31 | - Nodenext 32 | exclude: 33 | - package-json-type: commonjs 34 | tsconfig-json-module: ESNext 35 | - package-json-type: module 36 | tsconfig-json-module: CommonJS 37 | - package-json-type: module 38 | tsconfig-json-module: Node16 39 | tsconfig-json-module-resolution: Node 40 | - package-json-type: module 41 | tsconfig-json-module: NodeNext 42 | tsconfig-json-module-resolution: Node 43 | 44 | fail-fast: false 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | with: 49 | persist-credentials: false 50 | - uses: actions/setup-node@v4 51 | with: 52 | node-version: 18 53 | - run: | 54 | npm install --ignore-scripts && 55 | npm link && 56 | node scripts/create-ts-interop-test && 57 | cd test_interop && 58 | npm i --ignore-scripts && 59 | npm link fast-querystring && 60 | npm run test-interop 61 | env: 62 | PACKAGE_JSON_TYPE: ${{ matrix.package-json-type }} 63 | TSCONFIG_MODULE: ${{ matrix.tsconfig-json-module }} 64 | TSCONFIG_MODULE_RESOLUTION: ${{ matrix.tsconfig-json-module-resolution }} 65 | -------------------------------------------------------------------------------- /scripts/create-ts-interop-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require("path"); 3 | const fs = require("fs").promises; 4 | 5 | const packageJsonType = 6 | process.argv[2] || process.env.PACKAGE_JSON_TYPE || "commonjs"; 7 | const tsconfigModule = process.argv[2] || process.env.TSCONFIG_MODULE || "Node"; 8 | const tsconfigModuleResolution = 9 | process.argv[2] || process.env.TSCONFIG_MODULE_RESOLUTION || "Node"; 10 | 11 | const rootPath = path.join(__dirname, ".."); 12 | const testPath = path.join(rootPath, "test_interop"); 13 | 14 | const indexTs = `import fastQuerystring from 'fast-querystring' 15 | import { stringify, parse } from 'fast-querystring' 16 | import * as fQuerystring from 'fast-querystring' 17 | import { equal } from "assert" 18 | 19 | equal(typeof fastQuerystring, 'object') 20 | equal(typeof fastQuerystring.stringify, 'function') 21 | equal(typeof fastQuerystring.parse, 'function') 22 | equal(typeof fQuerystring.stringify, 'function') 23 | equal(typeof fQuerystring.parse, 'function') 24 | equal(typeof stringify, 'function') 25 | equal(typeof parse, 'function') 26 | `; 27 | 28 | const tsconfigJson = JSON.stringify( 29 | { 30 | compilerOptions: { 31 | module: tsconfigModule, 32 | moduleResolution: tsconfigModuleResolution, 33 | }, 34 | }, 35 | null, 36 | 2, 37 | ); 38 | 39 | const packageJson = JSON.stringify( 40 | { 41 | name: "fqs-test", 42 | version: "1.0.0", 43 | description: "", 44 | main: "index.js", 45 | type: packageJsonType, 46 | scripts: { 47 | "test-interop": "tsc -p . && node index.js", 48 | }, 49 | keywords: [], 50 | author: "", 51 | license: "ISC", 52 | dependencies: { 53 | "@types/node": "^18.11.10", 54 | typescript: "^4.9.3", 55 | }, 56 | }, 57 | null, 58 | 2, 59 | ); 60 | 61 | async function main() { 62 | await fs.mkdir(testPath); 63 | await fs.writeFile(path.join(testPath, "package.json"), packageJson); 64 | await fs.writeFile(path.join(testPath, "tsconfig.json"), tsconfigJson); 65 | await fs.writeFile(path.join(testPath, "index.ts"), indexTs); 66 | } 67 | 68 | main(process.argv).catch((err) => { 69 | console.error(err); 70 | process.exit(1); 71 | }); 72 | -------------------------------------------------------------------------------- /test/parse.test.ts: -------------------------------------------------------------------------------- 1 | import querystring from "querystring"; 2 | import { assert, test } from "vitest"; 3 | import qs from "../lib"; 4 | import { qsNoMungeTestCases, qsTestCases, qsWeirdObjects } from "./node"; 5 | 6 | test("should succeed on node.js tests", () => { 7 | qsWeirdObjects.forEach((t) => 8 | assert.deepEqual(qs.parse(t[1] as string), t[2] as Record), 9 | ); 10 | qsNoMungeTestCases.forEach((t) => assert.deepEqual(qs.parse(t[0]), t[1])); 11 | qsTestCases.forEach((t) => assert.deepEqual(qs.parse(t[0]), t[2])); 12 | }); 13 | 14 | test("native querystring module should match the test suite result", () => { 15 | qsTestCases.forEach((t) => assert.deepEqual(querystring.parse(t[0]), t[2])); 16 | qsNoMungeTestCases.forEach((t) => 17 | assert.deepEqual(querystring.parse(t[0]), t[1]), 18 | ); 19 | }); 20 | 21 | test("handles & on first/last character", () => { 22 | assert.deepEqual(qs.parse("&hello=world"), { hello: "world" }); 23 | assert.deepEqual(qs.parse("hello=world&"), { hello: "world" }); 24 | }); 25 | 26 | test("handles ? on first character", () => { 27 | // This aligns with `node:querystring` functionality 28 | assert.deepEqual(qs.parse("?hello=world"), { "?hello": "world" }); 29 | }); 30 | 31 | test("handles + character", () => { 32 | assert.deepEqual(qs.parse("author=Yagiz+Nizipli"), { 33 | author: "Yagiz Nizipli", 34 | }); 35 | }); 36 | 37 | test("should accept pairs with missing values", () => { 38 | assert.deepEqual(qs.parse("foo=bar&hey"), { foo: "bar", hey: "" }); 39 | assert.deepEqual(qs.parse("hey"), { hey: "" }); 40 | }); 41 | 42 | test("should decode key", () => { 43 | assert.deepEqual(qs.parse("invalid%key=hello"), { "invalid%key": "hello" }); 44 | assert.deepEqual(qs.parse("full%20name=Yagiz"), { "full name": "Yagiz" }); 45 | }); 46 | 47 | test("should handle really large object", () => { 48 | const query = {}; 49 | 50 | for (let i = 0; i < 2000; i++) query[i] = i; 51 | 52 | const url = qs.stringify(query); 53 | 54 | assert.strictEqual(Object.keys(qs.parse(url)).length, 2000); 55 | }); 56 | 57 | test("should parse large numbers", () => { 58 | assert.strictEqual( 59 | qs.parse("id=918854443121279438895193").id, 60 | "918854443121279438895193", 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /lib/internals/querystring.js: -------------------------------------------------------------------------------- 1 | // This file is taken from Node.js project. 2 | // Full implementation can be found from https://github.com/nodejs/node/blob/main/lib/internal/querystring.js 3 | 4 | const hexTable = Array.from( 5 | { length: 256 }, 6 | (_, i) => "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase(), 7 | ); 8 | 9 | // These characters do not need escaping when generating query strings: 10 | // ! - . _ ~ 11 | // ' ( ) * 12 | // digits 13 | // alpha (uppercase) 14 | // alpha (lowercase) 15 | // biome-ignore format: the array should not be formatted 16 | const noEscape = new Int8Array([ 17 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 18 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 19 | 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47 20 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 21 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 22 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95 23 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 24 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127 25 | ]); 26 | 27 | /** 28 | * @param {string} str 29 | * @returns {string} 30 | */ 31 | function encodeString(str) { 32 | const len = str.length; 33 | if (len === 0) return ""; 34 | 35 | let out = ""; 36 | let lastPos = 0; 37 | let i = 0; 38 | 39 | outer: for (; i < len; i++) { 40 | let c = str.charCodeAt(i); 41 | 42 | // ASCII 43 | while (c < 0x80) { 44 | if (noEscape[c] !== 1) { 45 | if (lastPos < i) out += str.slice(lastPos, i); 46 | lastPos = i + 1; 47 | out += hexTable[c]; 48 | } 49 | 50 | if (++i === len) break outer; 51 | 52 | c = str.charCodeAt(i); 53 | } 54 | 55 | if (lastPos < i) out += str.slice(lastPos, i); 56 | 57 | // Multi-byte characters ... 58 | if (c < 0x800) { 59 | lastPos = i + 1; 60 | out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)]; 61 | continue; 62 | } 63 | if (c < 0xd800 || c >= 0xe000) { 64 | lastPos = i + 1; 65 | out += 66 | hexTable[0xe0 | (c >> 12)] + 67 | hexTable[0x80 | ((c >> 6) & 0x3f)] + 68 | hexTable[0x80 | (c & 0x3f)]; 69 | continue; 70 | } 71 | // Surrogate pair 72 | ++i; 73 | 74 | // This branch should never happen because all URLSearchParams entries 75 | // should already be converted to USVString. But, included for 76 | // completion's sake anyway. 77 | if (i >= len) { 78 | throw new Error("URI malformed"); 79 | } 80 | 81 | const c2 = str.charCodeAt(i) & 0x3ff; 82 | 83 | lastPos = i + 1; 84 | c = 0x10000 + (((c & 0x3ff) << 10) | c2); 85 | out += 86 | hexTable[0xf0 | (c >> 18)] + 87 | hexTable[0x80 | ((c >> 12) & 0x3f)] + 88 | hexTable[0x80 | ((c >> 6) & 0x3f)] + 89 | hexTable[0x80 | (c & 0x3f)]; 90 | } 91 | if (lastPos === 0) return str; 92 | if (lastPos < len) return out + str.slice(lastPos); 93 | return out; 94 | } 95 | 96 | module.exports = { encodeString }; 97 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fastDecode = require("fast-decode-uri-component"); 4 | 5 | const plusRegex = /\+/g; 6 | const Empty = function () {}; 7 | Empty.prototype = Object.create(null); 8 | 9 | /** 10 | * @callback parse 11 | * @param {string} input 12 | */ 13 | function parse(input) { 14 | // Optimization: Use new Empty() instead of Object.create(null) for performance 15 | // v8 has a better optimization for initializing functions compared to Object 16 | const result = new Empty(); 17 | 18 | if (typeof input !== "string") { 19 | return result; 20 | } 21 | 22 | const inputLength = input.length; 23 | let key = ""; 24 | let value = ""; 25 | let startingIndex = -1; 26 | let equalityIndex = -1; 27 | let shouldDecodeKey = false; 28 | let shouldDecodeValue = false; 29 | let keyHasPlus = false; 30 | let valueHasPlus = false; 31 | let hasBothKeyValuePair = false; 32 | let c = 0; 33 | 34 | // Have a boundary of input.length + 1 to access last pair inside the loop. 35 | for (let i = 0; i < inputLength + 1; i++) { 36 | c = i !== inputLength ? input.charCodeAt(i) : 38; 37 | 38 | // Handle '&' and end of line to pass the current values to result 39 | if (c === 38) { 40 | hasBothKeyValuePair = equalityIndex > startingIndex; 41 | 42 | // Optimization: Reuse equality index to store the end of key 43 | if (!hasBothKeyValuePair) { 44 | equalityIndex = i; 45 | } 46 | 47 | key = input.slice(startingIndex + 1, equalityIndex); 48 | 49 | // Add key/value pair only if the range size is greater than 1; a.k.a. contains at least "=" 50 | if (hasBothKeyValuePair || key.length > 0) { 51 | // Optimization: Replace '+' with space 52 | if (keyHasPlus) { 53 | key = key.replace(plusRegex, " "); 54 | } 55 | 56 | // Optimization: Do not decode if it's not necessary. 57 | if (shouldDecodeKey) { 58 | key = fastDecode(key) || key; 59 | } 60 | 61 | if (hasBothKeyValuePair) { 62 | value = input.slice(equalityIndex + 1, i); 63 | 64 | if (valueHasPlus) { 65 | value = value.replace(plusRegex, " "); 66 | } 67 | 68 | if (shouldDecodeValue) { 69 | value = fastDecode(value) || value; 70 | } 71 | } 72 | const currentValue = result[key]; 73 | 74 | if (currentValue === undefined) { 75 | result[key] = value; 76 | } else { 77 | // Optimization: value.pop is faster than Array.isArray(value) 78 | if (currentValue.pop) { 79 | currentValue.push(value); 80 | } else { 81 | result[key] = [currentValue, value]; 82 | } 83 | } 84 | } 85 | 86 | // Reset reading key value pairs 87 | value = ""; 88 | startingIndex = i; 89 | equalityIndex = i; 90 | shouldDecodeKey = false; 91 | shouldDecodeValue = false; 92 | keyHasPlus = false; 93 | valueHasPlus = false; 94 | } 95 | // Check '=' 96 | else if (c === 61) { 97 | if (equalityIndex <= startingIndex) { 98 | equalityIndex = i; 99 | } 100 | // If '=' character occurs again, we should decode the input. 101 | else { 102 | shouldDecodeValue = true; 103 | } 104 | } 105 | // Check '+', and remember to replace it with empty space. 106 | else if (c === 43) { 107 | if (equalityIndex > startingIndex) { 108 | valueHasPlus = true; 109 | } else { 110 | keyHasPlus = true; 111 | } 112 | } 113 | // Check '%' character for encoding 114 | else if (c === 37) { 115 | if (equalityIndex > startingIndex) { 116 | shouldDecodeValue = true; 117 | } else { 118 | shouldDecodeKey = true; 119 | } 120 | } 121 | } 122 | 123 | return result; 124 | } 125 | 126 | module.exports = parse; 127 | -------------------------------------------------------------------------------- /test/stringify.test.ts: -------------------------------------------------------------------------------- 1 | import querystring from "querystring"; 2 | import { assert, test } from "vitest"; 3 | import qs from "../lib"; 4 | import { qsNoMungeTestCases, qsTestCases, qsWeirdObjects } from "./node"; 5 | 6 | test("should succeed on node.js tests", () => { 7 | qsWeirdObjects.forEach((t) => 8 | assert.deepEqual( 9 | qs.stringify(t[2] as Record), 10 | t[1] as string, 11 | ), 12 | ); 13 | qsNoMungeTestCases.forEach((t) => assert.deepEqual(qs.stringify(t[1]), t[0])); 14 | qsTestCases.forEach((t) => assert.deepEqual(qs.stringify(t[2]), t[1])); 15 | }); 16 | 17 | test("native querystring module should match the test suite result", () => { 18 | qsTestCases.forEach((t) => 19 | assert.deepEqual(querystring.stringify(t[2]), t[1]), 20 | ); 21 | qsNoMungeTestCases.forEach((t) => 22 | assert.deepEqual(querystring.stringify(t[1]), t[0]), 23 | ); 24 | }); 25 | 26 | test("should handle numbers", () => { 27 | assert.deepEqual( 28 | qs.stringify({ age: 5, name: "John Doe" }), 29 | "age=5&name=John%20Doe", 30 | ); 31 | }); 32 | 33 | test("should handle mixed ascii and non-ascii", () => { 34 | assert.deepEqual(qs.stringify({ name: "Jöhn Doe" }), "name=J%C3%B6hn%20Doe"); 35 | }); 36 | 37 | test("should handle BigInt", () => { 38 | assert.deepEqual( 39 | qs.stringify({ age: BigInt(55), name: "John" }), 40 | "age=55&name=John", 41 | ); 42 | assert.strictEqual(qs.stringify({ foo: 2n ** 1023n }), "foo=" + 2n ** 1023n); 43 | assert.strictEqual(qs.stringify([0n, 1n, 2n]), "0=0&1=1&2=2"); 44 | }); 45 | 46 | test("should handle boolean values", () => { 47 | assert.deepEqual(qs.stringify({ valid: true }), "valid=true"); 48 | assert.deepEqual(qs.stringify({ valid: false }), "valid=false"); 49 | }); 50 | 51 | test("should handle numbers", () => { 52 | assert.deepEqual(qs.stringify({ value: 1e22 }), "value=1e%2B22"); 53 | }); 54 | 55 | test("should omit objects", () => { 56 | // This aligns with querystring module 57 | assert.deepEqual(qs.stringify({ user: {} }), "user="); 58 | }); 59 | 60 | test("should omit non-object inputs", () => { 61 | assert.deepEqual(qs.stringify("hello" as never), ""); 62 | }); 63 | 64 | test("should handle utf16 characters", () => { 65 | assert.deepEqual(qs.stringify({ utf16: "ܩ" }), "utf16=%DC%A9"); 66 | assert.deepEqual(qs.stringify({ utf16: "睷" }), "utf16=%E7%9D%B7"); 67 | assert.deepEqual(qs.stringify({ utf16: "aܩ" }), "utf16=a%DC%A9"); 68 | assert.deepEqual(qs.stringify({ utf16: "a睷" }), "utf16=a%E7%9D%B7"); 69 | }); 70 | 71 | test("should handle multi-byte characters", () => { 72 | assert.deepEqual(qs.stringify({ multiByte: "𝌆" }), "multiByte=%F0%9D%8C%86"); 73 | }); 74 | 75 | test("invalid surrogate pair should throw", () => { 76 | assert.throws(() => qs.stringify({ foo: "\udc00" }), "URI malformed"); 77 | }); 78 | 79 | test("should omit nested values", () => { 80 | const f = qs.stringify({ 81 | a: "b", 82 | q: qs.stringify({ 83 | x: "y", 84 | y: "z", 85 | }), 86 | }); 87 | assert.strictEqual(f, "a=b&q=x%3Dy%26y%3Dz"); 88 | }); 89 | 90 | test("should coerce numbers to string", () => { 91 | assert.strictEqual(qs.stringify({ foo: 0 }), "foo=0"); 92 | assert.strictEqual(qs.stringify({ foo: -0 }), "foo=0"); 93 | assert.strictEqual(qs.stringify({ foo: 3 }), "foo=3"); 94 | assert.strictEqual(qs.stringify({ foo: -72.42 }), "foo=-72.42"); 95 | assert.strictEqual(qs.stringify({ foo: Number.NaN }), "foo="); 96 | assert.strictEqual(qs.stringify({ foo: 1e21 }), "foo=1e%2B21"); 97 | assert.strictEqual(qs.stringify({ foo: Number.POSITIVE_INFINITY }), "foo="); 98 | }); 99 | 100 | test("should return empty string on certain inputs", () => { 101 | assert.strictEqual(qs.stringify(undefined as never), ""); 102 | assert.strictEqual(qs.stringify(0 as never), ""); 103 | assert.strictEqual(qs.stringify([]), ""); 104 | assert.strictEqual(qs.stringify(null as never), ""); 105 | assert.strictEqual(qs.stringify(true as never), ""); 106 | }); 107 | -------------------------------------------------------------------------------- /test/node.ts: -------------------------------------------------------------------------------- 1 | import vm from "node:vm"; 2 | 3 | function extendedFunction() {} 4 | extendedFunction.prototype = { a: "b" }; 5 | 6 | function createWithNoPrototype(properties) { 7 | const noProto = Object.create(null); 8 | properties.forEach((property) => { 9 | noProto[property.key] = property.value; 10 | }); 11 | return noProto; 12 | } 13 | export const foreignObject = vm.runInNewContext('({"foo": ["bar", "baz"]})'); 14 | export const qsNoMungeTestCases = [ 15 | ["", {}], 16 | ["foo=bar&foo=baz", { foo: ["bar", "baz"] }], 17 | ["foo=bar&foo=baz", foreignObject], 18 | ["blah=burp", { blah: "burp" }], 19 | ["a=!-._~'()*", { a: "!-._~'()*" }], 20 | ["a=abcdefghijklmnopqrstuvwxyz", { a: "abcdefghijklmnopqrstuvwxyz" }], 21 | ["a=ABCDEFGHIJKLMNOPQRSTUVWXYZ", { a: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }], 22 | ["a=0123456789", { a: "0123456789" }], 23 | ["gragh=1&gragh=3&goo=2", { gragh: ["1", "3"], goo: "2" }], 24 | [ 25 | "frappucino=muffin&goat%5B%5D=scone&pond=moose", 26 | { frappucino: "muffin", "goat[]": "scone", pond: "moose" }, 27 | ], 28 | ["trololol=yes&lololo=no", { trololol: "yes", lololo: "no" }], 29 | ]; 30 | export const qsTestCases = [ 31 | [ 32 | "__proto__=1", 33 | "__proto__=1", 34 | createWithNoPrototype([{ key: "__proto__", value: "1" }]), 35 | ], 36 | [ 37 | "__defineGetter__=asdf", 38 | "__defineGetter__=asdf", 39 | JSON.parse('{"__defineGetter__":"asdf"}'), 40 | ], 41 | [ 42 | "foo=918854443121279438895193", 43 | "foo=918854443121279438895193", 44 | { foo: "918854443121279438895193" }, 45 | ], 46 | ["foo=bar", "foo=bar", { foo: "bar" }], 47 | ["foo=bar&foo=quux", "foo=bar&foo=quux", { foo: ["bar", "quux"] }], 48 | ["foo=1&bar=2", "foo=1&bar=2", { foo: "1", bar: "2" }], 49 | [ 50 | "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F", 51 | "my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F", 52 | { "my weird field": "q1!2\"'w$5&7/z8)?" }, 53 | ], 54 | ["foo%3Dbaz=bar", "foo%3Dbaz=bar", { "foo=baz": "bar" }], 55 | ["foo=baz=bar", "foo=baz%3Dbar", { foo: "baz=bar" }], 56 | [ 57 | "str=foo&arr=1&arr=2&arr=3&somenull=&undef=", 58 | "str=foo&arr=1&arr=2&arr=3&somenull=&undef=", 59 | { 60 | str: "foo", 61 | arr: ["1", "2", "3"], 62 | somenull: "", 63 | undef: "", 64 | }, 65 | ], 66 | [" foo = bar ", "%20foo%20=%20bar%20", { " foo ": " bar " }], 67 | ["foo=%zx", "foo=%25zx", { foo: "%zx" }], 68 | ["foo=%EF%BF%BD", "foo=%EF%BF%BD", { foo: "\ufffd" }], 69 | // See: https://github.com/joyent/node/issues/1707 70 | [ 71 | "hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz", 72 | "hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz", 73 | { 74 | hasOwnProperty: "x", 75 | toString: "foo", 76 | valueOf: "bar", 77 | __defineGetter__: "baz", 78 | }, 79 | ], 80 | // See: https://github.com/joyent/node/issues/3058 81 | ["foo&bar=baz", "foo=&bar=baz", { foo: "", bar: "baz" }], 82 | ["a=b&c&d=e", "a=b&c=&d=e", { a: "b", c: "", d: "e" }], 83 | ["a=b&c=&d=e", "a=b&c=&d=e", { a: "b", c: "", d: "e" }], 84 | ["a=b&=c&d=e", "a=b&=c&d=e", { a: "b", "": "c", d: "e" }], 85 | ["a=b&=&c=d", "a=b&=&c=d", { a: "b", "": "", c: "d" }], 86 | ["&&foo=bar&&", "foo=bar", { foo: "bar" }], 87 | ["&", "", {}], 88 | ["&&&&", "", {}], 89 | ["&=&", "=", { "": "" }], 90 | ["&=&=", "=&=", { "": ["", ""] }], 91 | ["=", "=", { "": "" }], 92 | ["+", "%20=", { " ": "" }], 93 | ["+=", "%20=", { " ": "" }], 94 | ["+&", "%20=", { " ": "" }], 95 | ["=+", "=%20", { "": " " }], 96 | ["+=&", "%20=", { " ": "" }], 97 | ["a&&b", "a=&b=", { a: "", b: "" }], 98 | ["a=a&&b=b", "a=a&b=b", { a: "a", b: "b" }], 99 | ["&a", "a=", { a: "" }], 100 | ["&=", "=", { "": "" }], 101 | ["a&a&", "a=&a=", { a: ["", ""] }], 102 | ["a&a&a&", "a=&a=&a=", { a: ["", "", ""] }], 103 | ["a&a&a&a&", "a=&a=&a=&a=", { a: ["", "", "", ""] }], 104 | ["a=&a=value&a=", "a=&a=value&a=", { a: ["", "value", ""] }], 105 | ["foo+bar=baz+quux", "foo%20bar=baz%20quux", { "foo bar": "baz quux" }], 106 | ["+foo=+bar", "%20foo=%20bar", { " foo": " bar" }], 107 | ["a+", "a%20=", { "a ": "" }], 108 | ["=a+", "=a%20", { "": "a " }], 109 | ["a+&", "a%20=", { "a ": "" }], 110 | ["=a+&", "=a%20", { "": "a " }], 111 | ["%20+", "%20%20=", { " ": "" }], 112 | ["=%20+", "=%20%20", { "": " " }], 113 | ["%20+&", "%20%20=", { " ": "" }], 114 | ["=%20+&", "=%20%20", { "": " " }], 115 | [null, "", {}], 116 | [undefined, "", {}], 117 | ]; 118 | export const qsWeirdObjects = [ 119 | [{ regexp: /./g }, "regexp=", { regexp: "" }], 120 | [{ regexp: /./g }, "regexp=", { regexp: "" }], 121 | [{ fn: () => {} }, "fn=", { fn: "" }], 122 | [{ fn: new Function("") }, "fn=", { fn: "" }], 123 | [{ math: Math }, "math=", { math: "" }], 124 | [{ e: extendedFunction }, "e=", { e: "" }], 125 | [{ d: new Date() }, "d=", { d: "" }], 126 | [{ d: Date }, "d=", { d: "" }], 127 | [{ f: new Boolean(false), t: new Boolean(true) }, "f=&t=", { f: "", t: "" }], 128 | [{ f: false, t: true }, "f=false&t=true", { f: "false", t: "true" }], 129 | [{ n: null }, "n=", { n: "" }], 130 | [{ nan: Number.NaN }, "nan=", { nan: "" }], 131 | [{ inf: Number.POSITIVE_INFINITY }, "inf=", { inf: "" }], 132 | [{ a: [], b: [] }, "", {}], 133 | [{ a: 1, b: [] }, "a=1", { a: "1" }], 134 | ]; 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-querystring 2 | 3 | ![Test](https://github.com/anonrig/fast-querystring/workflows/test/badge.svg) 4 | [![codecov](https://codecov.io/gh/anonrig/fast-querystring/branch/main/graph/badge.svg?token=4ZDJA2BMOH)](https://codecov.io/gh/anonrig/fast-querystring) 5 | [![NPM version](https://img.shields.io/npm/v/fast-querystring.svg?style=flat)](https://www.npmjs.com/package/fast-querystring) 6 | 7 | Fast query-string parser and stringifier to replace the legacy `node:querystring` module. 8 | 9 | ### Installation 10 | 11 | ``` 12 | npm i fast-querystring 13 | ``` 14 | 15 | ### Features 16 | 17 | - Supports both `parse` and `stringify` methods from `node:querystring` module 18 | - Parsed object does not have prototype methods 19 | - Uses `&` separator as default 20 | - Supports only input of type `string` 21 | - Supports repeating keys in query string 22 | - `foo=bar&foo=baz` parses into `{foo: ['bar', 'baz']}` 23 | - Supports pairs with missing values 24 | - `foo=bar&hola` parses into `{foo: 'bar', hola: ''}` 25 | - Stringify does not support nested values (just like `node:querystring`) 26 | 27 | ### Usage 28 | 29 | ```javascript 30 | const qs = require('fast-querystring') 31 | 32 | // Parsing a querystring 33 | console.log(qs.parse('hello=world&foo=bar&values=v1&values=v2')) 34 | // { 35 | // hello: 'world', 36 | // foo: 'bar', 37 | // values: ['v1', 'v2'] 38 | // } 39 | 40 | // Stringifying an object 41 | console.log(qs.stringify({ foo: ['bar', 'baz'] })) 42 | // 'foo=bar&foo=baz' 43 | ``` 44 | 45 | ### Benchmark 46 | 47 | All benchmarks are run using Node.js v20.2.0 running on M1 Max. 48 | 49 | - Parsing a query-string 50 | 51 | ``` 52 | > node benchmark/parse.mjs 53 | 54 | ╔═════════════════════════════════════════╤═════════╤═══════════════════╤═══════════╗ 55 | ║ Slower tests │ Samples │ Result │ Tolerance ║ 56 | ╟─────────────────────────────────────────┼─────────┼───────────────────┼───────────╢ 57 | ║ query-string │ 10000 │ 273968.62 op/sec │ ± 1.48 % ║ 58 | ║ qs │ 9999 │ 324118.68 op/sec │ ± 0.99 % ║ 59 | ║ querystringify │ 1000 │ 410157.64 op/sec │ ± 0.68 % ║ 60 | ║ @aws-sdk/querystring-parser │ 1000 │ 431465.20 op/sec │ ± 0.83 % ║ 61 | ║ URLSearchParams-with-Object.fromEntries │ 5000 │ 833939.19 op/sec │ ± 0.97 % ║ 62 | ║ URLSearchParams-with-construct │ 10000 │ 980017.92 op/sec │ ± 2.42 % ║ 63 | ║ node:querystring │ 10000 │ 1068165.86 op/sec │ ± 3.41 % ║ 64 | ║ querystringparser │ 3000 │ 1384001.31 op/sec │ ± 0.95 % ║ 65 | ╟─────────────────────────────────────────┼─────────┼───────────────────┼───────────╢ 66 | ║ Fastest test │ Samples │ Result │ Tolerance ║ 67 | ╟─────────────────────────────────────────┼─────────┼───────────────────┼───────────╢ 68 | ║ fast-querystring │ 10000 │ 1584458.62 op/sec │ ± 2.64 % ║ 69 | ╚═════════════════════════════════════════╧═════════╧═══════════════════╧═══════════╝ 70 | ``` 71 | 72 | - Stringify a query-string 73 | 74 | ``` 75 | > node benchmark/stringify.mjs 76 | 77 | ╔══════════════════════════════╤═════════╤═══════════════════╤═══════════╗ 78 | ║ Slower tests │ Samples │ Result │ Tolerance ║ 79 | ╟──────────────────────────────┼─────────┼───────────────────┼───────────╢ 80 | ║ query-string │ 10000 │ 314662.25 op/sec │ ± 1.08 % ║ 81 | ║ qs │ 9500 │ 353621.74 op/sec │ ± 0.98 % ║ 82 | ║ http-querystring-stringify │ 10000 │ 372189.04 op/sec │ ± 1.48 % ║ 83 | ║ @aws-sdk/querystring-builder │ 10000 │ 411658.63 op/sec │ ± 1.67 % ║ 84 | ║ URLSearchParams │ 10000 │ 454438.85 op/sec │ ± 1.32 % ║ 85 | ║ querystringparser │ 10000 │ 455615.18 op/sec │ ± 4.22 % ║ 86 | ║ querystringify │ 10000 │ 879020.96 op/sec │ ± 2.12 % ║ 87 | ║ querystringify-ts │ 10000 │ 879134.48 op/sec │ ± 2.19 % ║ 88 | ║ node:querystring │ 10000 │ 1244505.97 op/sec │ ± 2.12 % ║ 89 | ╟──────────────────────────────┼─────────┼───────────────────┼───────────╢ 90 | ║ Fastest test │ Samples │ Result │ Tolerance ║ 91 | ╟──────────────────────────────┼─────────┼───────────────────┼───────────╢ 92 | ║ fast-querystring │ 10000 │ 1953717.60 op/sec │ ± 3.16 % ║ 93 | ╚══════════════════════════════╧═════════╧═══════════════════╧═══════════╝ 94 | ``` 95 | 96 | - Importing package. 97 | 98 | ``` 99 | > node benchmark/import.mjs 100 | 101 | ╔═════════════════════════════╤═════════╤═════════════════╤═══════════╗ 102 | ║ Slower tests │ Samples │ Result │ Tolerance ║ 103 | ╟─────────────────────────────┼─────────┼─────────────────┼───────────╢ 104 | ║ @aws-sdk/querystring-parser │ 1000 │ 12360.51 op/sec │ ± 0.57 % ║ 105 | ║ qs │ 1000 │ 14507.74 op/sec │ ± 0.36 % ║ 106 | ║ querystringify │ 1000 │ 14750.53 op/sec │ ± 0.39 % ║ 107 | ║ query-string │ 1000 │ 16335.05 op/sec │ ± 0.87 % ║ 108 | ║ querystringparser │ 1000 │ 17018.50 op/sec │ ± 0.42 % ║ 109 | ╟─────────────────────────────┼─────────┼─────────────────┼───────────╢ 110 | ║ Fastest test │ Samples │ Result │ Tolerance ║ 111 | ╟─────────────────────────────┼─────────┼─────────────────┼───────────╢ 112 | ║ fast-querystring │ 2500 │ 74605.83 op/sec │ ± 0.91 % ║ 113 | ╚═════════════════════════════╧═════════╧═════════════════╧═══════════╝ 114 | ``` 115 | --------------------------------------------------------------------------------