Skip to content

Commit b0ff11f

Browse files
committed
perf: improve parser performance
1 parent 223caf5 commit b0ff11f

3 files changed

Lines changed: 55 additions & 36 deletions

File tree

lib/parse.js

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
* @param {string} input
66
*/
77
function parse(input) {
8-
let result = Object.create(null);
8+
const result = Object.create(null);
99

10-
let currentKey = "";
11-
let currentValue = "";
12-
let seenEqualOperator = false;
10+
let key = "";
11+
let value = "";
12+
let separatorIndex = 0;
13+
let equalityIndex = 0;
14+
let shouldEncode = false;
1315

1416
// Have a boundary of input.length + 1 to access last pair inside the loop.
1517
for (let i = 0; i < input.length + 1; i++) {
@@ -18,29 +20,44 @@ function parse(input) {
1820
// Handle '&' and end of line to pass the current values to result
1921
if (c === 38 || isNaN(c)) {
2022
// Disallow empty key values.
21-
if (currentKey.length === 0) {
22-
continue
23-
} else if (typeof result[currentKey] === "undefined") {
24-
result[currentKey] = currentValue;
25-
} else if (Array.isArray(result[currentKey])) {
26-
result[currentKey].push(currentValue);
27-
} else {
28-
result[currentKey] = [result[currentKey], currentValue];
23+
if (equalityIndex - separatorIndex > 0 && i - equalityIndex + 1 > 0) {
24+
key = input.slice(separatorIndex, equalityIndex);
25+
value = input.slice(equalityIndex + 1, i);
26+
27+
// Optimization: Do not decode if it's not necessary.
28+
if (shouldEncode) {
29+
key = decodeURIComponent(key);
30+
value = decodeURIComponent(value);
31+
}
32+
33+
if (result[key] === undefined) {
34+
result[key] = value;
35+
} else {
36+
const currentValue = result[key];
37+
38+
if (currentValue.pop) {
39+
currentValue.push(value);
40+
} else {
41+
result[key] = [currentValue, value];
42+
}
43+
}
2944
}
3045

3146
// Reset reading key value pairs
32-
currentKey = "";
33-
currentValue = "";
34-
seenEqualOperator = false;
47+
separatorIndex = i + 1;
48+
equalityIndex = i + 1;
3549
}
3650
// Handle equal operator
3751
else if (c === 61) {
38-
seenEqualOperator = true;
52+
equalityIndex = i;
53+
}
54+
// Check '+', and replace it with empty space.
55+
else if (c === 43) {
56+
input[i] = " ";
3957
} else {
40-
if (seenEqualOperator) {
41-
currentValue += input[i];
42-
} else {
43-
currentKey += input[i];
58+
// Check '%' character for encoding
59+
if (c === 37) {
60+
shouldEncode = true;
4461
}
4562
}
4663
}

test/basics.test.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
import qs from "../lib";
22
import { test, assert } from "vitest";
3-
import vm from 'node:vm'
3+
import vm from "node:vm";
44

55
const foreignObject = vm.runInNewContext('({"foo": ["bar", "baz"]})');
66
const qsNoMungeTestCases = [
7-
['', {}],
8-
['foo=bar&foo=baz', { 'foo': ['bar', 'baz'] }],
9-
['foo=bar&foo=baz', foreignObject],
10-
['blah=burp', { 'blah': 'burp' }],
11-
['a=!-._~\'()*', { 'a': '!-._~\'()*' }],
12-
['a=abcdefghijklmnopqrstuvwxyz', { 'a': 'abcdefghijklmnopqrstuvwxyz' }],
13-
['a=ABCDEFGHIJKLMNOPQRSTUVWXYZ', { 'a': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }],
14-
['a=0123456789', { 'a': '0123456789' }],
15-
['gragh=1&gragh=3&goo=2', { 'gragh': ['1', '3'], 'goo': '2' }],
16-
['frappucino=muffin&goat%5B%5D=scone&pond=moose',
17-
{ 'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose' }],
18-
['trololol=yes&lololo=no', { 'trololol': 'yes', 'lololo': 'no' }],
7+
["", {}],
8+
["foo=bar&foo=baz", { foo: ["bar", "baz"] }],
9+
["foo=bar&foo=baz", foreignObject],
10+
["blah=burp", { blah: "burp" }],
11+
["a=!-._~'()*", { a: "!-._~'()*" }],
12+
["a=abcdefghijklmnopqrstuvwxyz", { a: "abcdefghijklmnopqrstuvwxyz" }],
13+
["a=ABCDEFGHIJKLMNOPQRSTUVWXYZ", { a: "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }],
14+
["a=0123456789", { a: "0123456789" }],
15+
["gragh=1&gragh=3&goo=2", { gragh: ["1", "3"], goo: "2" }],
16+
[
17+
"frappucino=muffin&goat%5B%5D=scone&pond=moose",
18+
{ frappucino: "muffin", "goat[]": "scone", pond: "moose" },
19+
],
20+
["trololol=yes&lololo=no", { trololol: "yes", lololo: "no" }],
1921
];
2022

2123
test("should parse the basics", () => {
2224
qsNoMungeTestCases.forEach((t) => {
23-
assert.deepEqual(qs.parse(t[0]), t[1])
24-
})
25+
assert.deepEqual(qs.parse(t[0]), t[1]);
26+
});
2527
});

test/benchmark.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import queryString from "query-string";
88
await benchmark(
99
{
1010
URLStateMachine() {
11-
return new URLStateMachine("hello=world&foo=bar", null, null, 115);
11+
return new URLStateMachine("hello=world&foo=bar", undefined, null, 115);
1212
},
1313
qs() {
1414
return qs.parse("hello=world&foo=bar");

0 commit comments

Comments
 (0)