Skip to content

Perf: JSON.parse is 4.5–7.7× and object construction 14–47× slower than Node / Bun / Deno in a hot loop (follow-up to #37) #61

Closed

Description

@hassidan

Test I ran

json_breakdown.js times the four phases of the benchmarks/controlled/json.js workload separately with performance.now(): build 10,000 small objects, JSON.stringify the array, JSON.parse it, sum one field. 10 warm-up + 30 measured iterations, median per phase. Run three times per runtime; trials agreed within 1%.

slice run json_breakdown.js --fuel=100000000000
node json_breakdown.js
bun json_breakdown.js
deno run json_breakdown.js
json_breakdown.js
// Phase breakdown of the benchmarks/controlled/json.js workload (n = 10,000 records).
// Each phase is timed separately with performance.now(); 10 warm-up + 30 measured iterations; median reported.
const N = 10000, SEED = 123;
function build() { const r = []; for (let i = 0; i < N; i++) r.push({ id: i, value: (i * 17 + SEED) % 100003, label: "row-" + i }); return r; }
function sum(decoded) { let s = 0; for (let i = 0; i < decoded.length; i++) s = (s + decoded[i].value) % 1000000007; return s; }
const T = { build: [], stringify: [], parse: [], sum: [] };
let check = 0;
for (let it = 0; it < 40; it++) {
  const t0 = performance.now(); const recs = build();
  const t1 = performance.now(); const text = JSON.stringify(recs);
  const t2 = performance.now(); const dec = JSON.parse(text);
  const t3 = performance.now(); check = sum(dec) + text.length;
  const t4 = performance.now();
  if (it >= 10) { T.build.push(t1 - t0); T.stringify.push(t2 - t1); T.parse.push(t3 - t2); T.sum.push(t4 - t3); }
}
const med = (a) => { a.sort((x, y) => x - y); return a[a.length >> 1]; };
const out = { check }; let total = 0;
for (const k of Object.keys(T)) { out[k + "_ms"] = +med(T[k]).toFixed(3); total += med(T[k]); }
out.total_ms = +total.toFixed(3);
console.log(JSON.stringify(out));

Result (ms, median, lower is better)

Phase Slice v0.10.7 Slice v0.10.5 Node v24.22.2 Bun 1.4.4 Deno 2.9.8 Slice vs slowest other
build 10,000 object literals 6.15 6 0.13 0.45 0.29 13.7× slower
JSON.stringify (275 KB out) 0.79 17 0.98 0.59 0.61 1.2× faster
JSON.parse 7.75 8 1.73 1.00 1.46 4.5× slower
sum loop (10,000 property reads) 1.16 1 0.035 0.033 0.036 32× slower
total 15.8 32 2.9 2.0 2.4 5.4× slower

(v0.10.5's performance.now() had 1 ms resolution.) All runtimes return the same checksum.

The independent harness run (benchmarks/controlled/run.py, warm engine, 90 calls) agrees in direction: JSON n=10,000 takes 26.8 ms on Slice v0.10.7 against 5.5 (Node), 4.0 (Deno), 3.9 (Bun); it was 55.7 ms on v0.10.5.

Reading

  • The JSON.stringify fast path from perf,feat: v0.10.6 JSON.stringify fast-path, HTTP dispatch, source maps, fs/readline #41 works: it went from 17 ms to 0.79 ms and now beats Node.
  • JSON.parse did not move (8 → 7.75 ms) and is the largest single phase. Note that making it correctly rounded (see the JSON.parse precision issue; serde_json's float_roundtrip is documented as roughly 2× slower for floats) will add cost here, so the two are worth looking at together.
  • Building objects and reading properties are 14–47× slower than the JIT runtimes. That is the interpreter-vs-JIT gap and I am recording the number, not suggesting it is a defect. For this workload it is 46% of Slice's total.

Why I am filing it

My threshold for "not acceptable" in this run was: slower than every one of Node, Bun and Deno on the same test. Three of the four phases meet it. For Slice's per-request model this is the cost of handling a ~275 KB JSON body: about 16 ms of CPU against 2–3 ms.

Environment

  • Slice v0.10.7, commit ea7c70c, cargo +1.96.0 build --release --locked; Slice v0.10.5 tag built the same way
  • Ubuntu 26.04, Linux 7.0, x86_64, i5-13500H, machine otherwise idle

Activity

  1. siktec-lab commented on Nov 11, 2026

    @siktec-lab
    Collaborator

    Addressed in PR #65 (v0.10.9) via the iterative ECMA-404 JSON.parse engine (parse_ecma404) and PropertyMap::insert_data_property:

    • Zero-copy JsStr::latin1 / utf16 slicing for unescaped strings and memcpy chunking into a 1-byte Latin-1 buffer for ASCII escapes (\n, \t, \", \\).
    • Unboxed 15-digit exact integer accumulator + fast-float2 for floats.
    • Direct PropertyMap::insert_data_property slot insertion on fresh objects without PropertyDescriptor or PropertyKey cloning overhead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions