diff --git a/bench/snippets/response-json.mjs b/bench/snippets/response-json.mjs index 2cd20523b6..28cad6e6c7 100644 --- a/bench/snippets/response-json.mjs +++ b/bench/snippets/response-json.mjs @@ -112,12 +112,40 @@ const obj = { }, }; -bench("Response.json(obj)", async () => { +const smallObj = { id: 1, name: "test" }; + +const arrayObj = { + items: Array.from({ length: 100 }, (_, i) => ({ id: i, value: `item-${i}` })), +}; + +bench("Response.json(obj)", () => { return Response.json(obj); }); -bench("Response.json(obj).json()", async () => { - return await Response.json(obj).json(); +bench("new Response(JSON.stringify(obj))", () => { + return new Response(JSON.stringify(obj), { + headers: { "Content-Type": "application/json" }, + }); +}); + +bench("Response.json(smallObj)", () => { + return Response.json(smallObj); +}); + +bench("new Response(JSON.stringify(smallObj))", () => { + return new Response(JSON.stringify(smallObj), { + headers: { "Content-Type": "application/json" }, + }); +}); + +bench("Response.json(arrayObj)", () => { + return Response.json(arrayObj); +}); + +bench("new Response(JSON.stringify(arrayObj))", () => { + return new Response(JSON.stringify(arrayObj), { + headers: { "Content-Type": "application/json" }, + }); }); await run(); diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index fe8b178dc1..8d911d6ecc 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -1202,6 +1202,15 @@ pub const JSValue = enum(i64) { return bun.jsc.fromJSHostCallGeneric(globalThis, @src(), JSC__JSValue__jsonStringify, .{ this, globalThis, indent, out }); } + extern fn JSC__JSValue__jsonStringifyFast(this: JSValue, globalThis: *JSGlobalObject, out: *bun.String) void; + + /// Fast version of JSON.stringify that uses JSC's FastStringifier optimization. + /// When space is undefined (as opposed to 0), JSC uses a highly optimized SIMD-based + /// serialization path. This is significantly faster for most common use cases. + pub fn jsonStringifyFast(this: JSValue, globalThis: *JSGlobalObject, out: *bun.String) bun.JSError!void { + return bun.jsc.fromJSHostCallGeneric(globalThis, @src(), JSC__JSValue__jsonStringifyFast, .{ this, globalThis, out }); + } + /// Call `toString()` on the JSValue and clone the result. pub fn toSliceOrNull(this: JSValue, globalThis: *JSGlobalObject) bun.JSError!ZigString.Slice { const str = try bun.String.fromJS(this, globalThis); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index ebaecfbe4d..3a4130c983 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -2552,6 +2552,22 @@ void JSC__JSValue__jsonStringify(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObje RETURN_IF_EXCEPTION(scope, ); *arg3 = Bun::toStringRef(str); } + +// Fast version of JSON.stringify that uses JSC's FastStringifier optimization. +// When space is undefined, JSC uses FastStringifier which is significantly faster +// than the general Stringifier used when space is a number (even 0). +void JSC__JSValue__jsonStringifyFast(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, + BunString* arg3) +{ + ASSERT_NO_PENDING_EXCEPTION(arg1); + auto& vm = JSC::getVM(arg1); + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSValue value = JSC::JSValue::decode(JSValue0); + // Passing jsUndefined() for space triggers JSC's FastStringifier optimization + WTF::String str = JSC::JSONStringify(arg1, value, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + *arg3 = Bun::toStringRef(str); +} unsigned char JSC__JSValue__jsType(JSC::EncodedJSValue JSValue0) { JSC::JSValue jsValue = JSC::JSValue::decode(JSValue0); diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index 06e1a7f96a..895bb4f078 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -265,6 +265,7 @@ CPP_DECL JSC::EncodedJSValue JSC__JSValue__jsNumberFromDouble(double arg0); CPP_DECL JSC::EncodedJSValue JSC__JSValue__jsNumberFromInt64(int64_t arg0); CPP_DECL JSC::EncodedJSValue JSC__JSValue__jsNumberFromU16(uint16_t arg0); CPP_DECL void JSC__JSValue__jsonStringify(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, uint32_t arg2, BunString* arg3); +CPP_DECL void JSC__JSValue__jsonStringifyFast(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, BunString* arg3); CPP_DECL JSC::EncodedJSValue JSC__JSValue__jsTDZValue(); CPP_DECL unsigned char JSC__JSValue__jsType(JSC::EncodedJSValue JSValue0); CPP_DECL JSC::EncodedJSValue JSC__JSValue__keys(JSC::JSGlobalObject* arg0, JSC::EncodedJSValue arg1); diff --git a/src/bun.js/webcore/Response.zig b/src/bun.js/webcore/Response.zig index b02967ccb6..4c43c933bc 100644 --- a/src/bun.js/webcore/Response.zig +++ b/src/bun.js/webcore/Response.zig @@ -529,10 +529,12 @@ pub fn constructJSON( const err = globalThis.createTypeErrorInstance("Do not know how to serialize a BigInt", .{}); return globalThis.throwValue(err); } + var str = bun.String.empty; - // calling JSON.stringify on an empty string adds extra quotes - // so this is correct - try json_value.jsonStringify(globalThis, 0, &str); + // Use jsonStringifyFast which passes undefined for the space parameter, + // triggering JSC's FastStringifier optimization. This is significantly faster + // than jsonStringify which passes 0 for space and uses the slower Stringifier. + try json_value.jsonStringifyFast(globalThis, &str); if (globalThis.hasException()) { return .zero; @@ -896,8 +898,6 @@ inline fn emptyWithStatus(_: *jsc.JSGlobalObject, status: u16) Response { /// https://developer.mozilla.org/en-US/docs/Web/API/Headers // TODO: move to http.zig. this has nothing to do with jsc or WebCore -const string = []const u8; - const std = @import("std"); const Method = @import("../../http/Method.zig").Method;