قلّل حجم JSON في Node.js API قبل ما تزود السيرفرات
مستوى القارئ: متوسط
هتخرج من المقال بخطة عملية تقلل حجم استجابة JSON وتخفض P95 latency قبل ما تدفع فلوس في سيرفرات زيادة.
المشكلة باختصار
لو عندك endpoint في Node.js بيرجع قائمة منتجات أو طلبات أو مستخدمين، أسهل غلطة هي إنك ترجع كل الحقول مرة واحدة. الطريقة دي بتفشل لما الداتا تكبر: الشبكة تتعب، المتصفح يقعد يفك JSON كبير، والـ API يظهر بطيء حتى لو قاعدة البيانات كويسة.
الافتراض إن عندك REST API مبني بـ Express، وعدد الطلبات حوالي 200 إلى 1000 طلب في الدقيقة. عندك endpoint مثل /api/orders بيرجع 500 order، وكل order فيه customer كامل، address، notes، audit fields، وitems. في قياس واقعي تقديري، الاستجابة ممكن تبقى 1.8MB وP95 حوالي 1200ms على اتصال متوسط. بعد تقليل الحقول والصفحات والضغط، ممكن تنزل إلى 180KB وP95 حوالي 380ms. ركز: الرقم ده تقديري للتوضيح، مش وعد ثابت.
ابدأ بالقياس بدل التخمين
أفضل طريقة هنا إنك تقيس حاجتين قبل أي تعديل: حجم الـ payload وP95 latency. حجم الاستجابة بيقولك هل المشكلة في البيانات المنقولة. P95 بيقولك 95% من الطلبات خلصت تحت زمن معين، وده أهم من المتوسط لما عندك مستخدمين حقيقيين.
جرّب الأمر ده من جهاز قريب من بيئة الاختبار:
# قياس الحجم الفعلي للاستجابة
curl -s -o /tmp/orders.json -w "size=%{size_download} bytes time=%{time_total}s\n" \
"https://api.example.com/api/orders?limit=500"
# قياس ضغط بسيط باستخدام wrk لمدة 30 ثانية
wrk -t4 -c80 -d30s "https://api.example.com/api/orders?limit=500"
لو الحجم فوق 500KB لصفحة عادية، غالبًا عندك فرصة تحسين كبيرة. لو الحجم 40KB والـ P95 عالي، المشكلة غالبًا في query، lock، أو خدمة خارجية. متخلطش المشاكل.
قلّل الحقول قبل ما تضغط
الضغط مهم، لكنه مش بديل عن تقليل البيانات. بدل ما ترجع كل order، رجّع الحقول المطلوبة للشاشة الحالية فقط. مثال: صفحة جدول الطلبات غالبًا تحتاج id، status، total، createdAt، واسم العميل. مش محتاجة notes ولا audit trail.
import express from "express";
import compression from "compression";
const app = express();
app.use(compression({ threshold: 1024 }));
app.get("/api/orders", async (req, res) => {
const limit = Math.min(Number(req.query.limit ?? 50), 100);
const cursor = req.query.cursor ?? null;
const rows = await db.orders.findMany({
take: limit + 1,
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
orderBy: { id: "desc" },
select: {
id: true,
status: true,
total: true,
createdAt: true,
customer: { select: { name: true } }
}
});
const hasMore = rows.length > limit;
const data = rows.slice(0, limit);
res.json({
data,
nextCursor: hasMore ? data[data.length - 1].id : null
});
});
اللي بيحصل فعلاً إنك بتكسب من 3 أماكن: قاعدة البيانات ترجع أعمدة أقل، Node.js يعمل JSON.stringify على object أصغر، والمتصفح يستقبل ويفك بيانات أقل. الـ trade-off هنا إن كل شاشة لازم تعرف احتياجها بوضوح. لو عندك clients كتير، ممكن تحتاج versioning أو endpoint منفصل لكل use case.
استخدم pagination صح
الـ pagination مش رفاهية. لو رجعت 500 عنصر لأن “الشاشة ممكن تحتاجهم”، أنت بتنقل تكلفة قرار UI للـ backend والمستخدم. الأفضل حد أقصى واضح، مثل 50 أو 100 عنصر، مع cursor بدل offset لما البيانات بتتغير بسرعة.
سيناريو واقعي: موقع داخلي عنده 50K طلب يوميًا، وفريق الدعم بيفتح صفحة الطلبات 300 مرة في اليوم. لو كل فتح بيرجع 1.8MB، أنت بتنقل حوالي 540MB يوميًا من endpoint واحد فقط. لو نزلت الاستجابة إلى 180KB، الرقم يبقى حوالي 54MB. بتكسب bandwidth وزمن تحميل. بتخسر إن المستخدم لازم يضغط “التالي” أو يعمل بحث أدق.
اضغط الاستجابة لكن افهم الثمن
HTTP compression بيستخدم Content-Encoding زي gzip أو br علشان يقلل البيانات المنقولة بدون تغيير نوع المحتوى الأصلي. MDN بتوضح إن الضغط مناسب للنصوص، لكنه مش مناسب غالبًا لملفات مضغوطة أصلًا مثل zip أو jpeg. في Node.js، مكتبة zlib بتوفر gzip وdeflate وBrotli، وExpress middleware مثل compression بيضيفها بسهولة.
الـ trade-off هنا واضح: بتكسب نقل أقل على الشبكة، وبتخسر CPU زيادة على السيرفر. لو الاستجابة 20KB، الضغط ممكن يضيف تكلفة بدون مكسب ملحوظ. لو الاستجابة 1MB JSON، الضغط غالبًا يستاهل. ابدأ بـ threshold مثل 1KB أو 2KB، وقيس CPU بعد النشر.
متى لا تستخدم هذه الطريقة
لا تستخدم تقليل الحقول عشوائيًا لو عندك contract عام يعتمد عليه عملاء خارجيون بدون versioning. لا تضيف compression لو السيرفر أصلًا CPU-bound، أو لو الـ CDN والـ reverse proxy بيضغطوا نفس الاستجابة. لا تستخدم cursor pagination لو المستخدم محتاج يقفز مباشرة للصفحة 70؛ هنا offset أو search index قد يكون أنسب.
مصادر اعتمد عليها
- MDN: Content-Encoding لشرح gzip وbr ومتى يكون الضغط مناسبًا.
- Node.js zlib documentation لمعرفة دعم gzip وBrotli في Node.js.
- Express compression middleware لاستخدام الضغط مع Express.
الخطوة التالية
افتح أبطأ endpoint بيرجع JSON عندك، وقس الحجم بـ curl. لو أكبر من 500KB، قلّل الحقول وطبّق limit لا يتجاوز 100، ثم أعد قياس P95 قبل التفكير في سيرفرات جديدة.