Why JSON Performance Matters
JSON serialization and parsing are among the most common operations in backend services. A fast API returning 1,000 requests per second spends a significant fraction of that time in JSON.stringify and JSON.parse. Small improvements compound: 10% faster JSON = 10% more API throughput with no hardware changes.
Tip 1: Minify Production Responses
Never send formatted (indented) JSON in production API responses. Whitespace is pure overhead:
// Development — readable but large
res.json(data); // Express default: no indent = already minified
// NEVER do this in production:
res.send(JSON.stringify(data, null, 2)); // 2-space indent wastes bandwidthA typical REST API response with 4-space indent is 40–55% larger than the same minified response. Use JSONKit's Minifier to measure the exact saving for your payload.
Tip 2: Enable HTTP Compression
HTTP gzip or Brotli compression on JSON responses reduces payload size by 60–80% on top of minification — because JSON has highly repetitive key names that compress extremely well.
Node.js / Express:
npm install compressionconst compression = require("compression");
app.use(compression()); // gzip-compresses all responses over 1 KB automaticallyNginx:
gzip on;
gzip_types application/json text/plain;
gzip_min_length 1024;Brotli is typically 15–20% better than gzip for JSON. Enable both and let the client choose with the Accept-Encoding header.
Tip 3: Return Only the Fields You Need
The most impactful optimization is to simply send less data. Audit every API response and remove fields the client never reads:
// Return only what's needed for a user list view
app.get("/users", async (req, res) => {
const users = await User.find().select("id name avatarUrl"); // not the full object
res.json(users);
});Support field selection via query parameter (like GraphQL or sparse fieldsets):
// GET /users?fields=id,name,email
app.get("/users", async (req, res) => {
const fields = req.query.fields?.split(",") ?? null;
const projection = fields ? Object.fromEntries(fields.map(f => [f, 1])) : null;
const users = await User.find({}, projection);
res.json(users);
});Tip 4: Paginate Large Collections
Never return unbounded JSON arrays. Pagination reduces both payload size and database load:
app.get("/products", async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // cap at 100
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
Product.find().skip(skip).limit(limit).lean(),
Product.countDocuments(),
]);
res.json({
data: items,
meta: { page, limit, total, pages: Math.ceil(total / limit) },
});
});Use .lean() in Mongoose to return plain JavaScript objects instead of Mongoose documents — this alone can speed up serialization by 2–5×.
Tip 5: Fast JSON Stringify with Schema
fast-json-stringify uses a JSON Schema to generate a highly optimized serializer — up to 5× faster than JSON.stringify for known shapes:
npm install fast-json-stringifyconst fastJson = require("fast-json-stringify");
const stringify = fastJson({
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
email: { type: "string" },
score: { type: "number" },
},
required: ["id", "name", "email"],
});
app.get("/users/:id", async (req, res) => {
const user = await User.findById(req.params.id).lean();
res.setHeader("Content-Type", "application/json");
res.send(stringify(user)); // 3–5× faster than JSON.stringify
});This approach is used by Fastify internally and is well-suited for high-throughput endpoints.
Tip 6: Streaming for Large Responses
For large datasets (thousands of objects), stream JSON instead of building the entire array in memory:
app.get("/export/users", async (req, res) => {
res.setHeader("Content-Type", "application/json");
res.write("[");
let first = true;
const cursor = User.find().lean().cursor(); // MongoDB stream cursor
for await (const user of cursor) {
if (!first) res.write(",");
res.write(JSON.stringify(user));
first = false;
}
res.write("]");
res.end();
});Or use NDJSON (one JSON object per line) — simpler to stream and parse incrementally:
app.get("/export/users.ndjson", async (req, res) => {
res.setHeader("Content-Type", "application/x-ndjson");
const cursor = User.find().lean().cursor();
for await (const user of cursor) {
res.write(JSON.stringify(user) + "\n");
}
res.end();
});Tip 7: JSON Caching
Cache serialized JSON strings — not JavaScript objects — when the same response is served repeatedly:
const cache = new Map();
app.get("/config", async (req, res) => {
const cacheKey = "config";
if (cache.has(cacheKey)) {
res.setHeader("Content-Type", "application/json");
return res.send(cache.get(cacheKey)); // serve pre-serialized string
}
const config = await Config.findOne().lean();
const json = JSON.stringify(config);
cache.set(cacheKey, json);
res.setHeader("Content-Type", "application/json");
res.send(json);
});This avoids calling JSON.stringify on every request for stable data.
Tip 8: Avoid JSON.parse(JSON.stringify()) for Deep Clone
This pattern is slow and loses type information:
// Slow and lossy — Dates become strings, undefined keys are dropped
const clone = JSON.parse(JSON.stringify(obj));
// Use structuredClone instead (Node 17+, all modern browsers)
const clone = structuredClone(obj); // fast, preserves Dates, Maps, SetsBenchmarking JSON Performance
Measure before optimizing. Use Node.js's built-in Perf Hooks:
const { performance } = require("perf_hooks");
const data = require("./large-dataset.json");
const t0 = performance.now();
const json = JSON.stringify(data);
const t1 = performance.now();
console.log(`JSON.stringify: ${(t1 - t0).toFixed(2)}ms, ${json.length} bytes`);
const t2 = performance.now();
JSON.parse(json);
const t3 = performance.now();
console.log(`JSON.parse: ${(t3 - t2).toFixed(2)}ms`);For micro-benchmarks, use the autocannon or wrk HTTP load testing tools to measure end-to-end throughput.
Performance Checklist
- Minify all production API responses
- Enable gzip or Brotli compression on the server
- Return only required fields — audit every endpoint
- Paginate all collection endpoints
- Use
.lean()in Mongoose and equivalents in other ORMs - Consider
fast-json-stringifyfor hot serialization paths - Stream large datasets instead of loading into memory
- Cache serialized JSON strings for stable, frequently-served responses
- Use
structuredCloneinstead of the JSON round-trip for deep cloning