ETag للـ API: قلّل نقل JSON المتكرر من 8MB لـ 530KB
هتطلع من المقال ده بطريقة عملية تقلل نقل JSON المتكرر في API بدون تغيير شكل الواجهة أو نقل الداتا لقاعدة جديدة.
مستوى القارئ: متوسط
المشكلة باختصار
لو عندك صفحة منتجات بتطلب /api/products كل دقيقة، غالبًا بتبعت نفس الرد كاملًا حتى لو الداتا لم تتغير. الطريقة دي بتفشل مع تطبيقات dashboards والموبايل، لأن كل refresh بيكلف bandwidth وCPU ووقت انتظار.
الافتراض هنا إن الرد حجمه حوالي 82KB، وعندك 100 طلب متكرر من نفس العملاء خلال فترة قصيرة. بدون ETag هتنقل تقريبًا 8.2MB. مع ETag، أول طلب فقط يرجع JSON، وبعده الطلبات المطابقة ترجع 304 Not Modified بدون body تقريبًا. الرقم التقريبي ينزل إلى 530KB بسبب headers وبعض الطلبات غير المطابقة.
المثال قبل المفهوم
ركز في السيناريو ده: عندك لوحة تحكم تعرض آخر الطلبات. الموظف يفتحها طول اليوم. الواجهة تعمل polling كل 60 ثانية. في أغلب الدقائق لا يوجد طلب جديد، لكن السيرفر ما زال يبعت نفس JSON كاملًا.
ETag هنا يشتغل كرقم نسخة للرد. أول مرة السيرفر يقول للعميل: هذا الرد نسخته "abc123". في الطلب التالي، العميل يرسل If-None-Match: "abc123". لو النسخة لم تتغير، السيرفر يرد 304 بدل ما يرسل JSON مرة ثانية.
علميًا، ETag هو entity tag يمثل نسخة المورد. وIf-None-Match يجعل طلب GET مشروطًا. حسب MDN، لو الـ ETag عند السيرفر يطابق قيمة If-None-Match، فالرد المناسب في GET/HEAD هو 304 Not Modified مع headers التخزين المهمة.
تطبيق عملي في Express
أفضل طريقة في API صغير أو متوسط هي حساب hash من JSON النهائي. هذا ليس أسرع شيء في العالم، لكنه واضح وسهل القياس. الـ trade-off هنا: هتكسب تقليل نقل الشبكة، وتخسر وقت CPU بسيط لحساب hash.
import express from "express";
import { createHash } from "node:crypto";
const app = express();
function strongEtag(payload) {
return '"' + createHash("sha256")
.update(payload)
.digest("base64url") + '"';
}
app.get("/api/products", async (req, res) => {
const products = await loadProductsFromCacheOrDb();
const body = JSON.stringify({ products });
const etag = strongEtag(body);
res.set("Cache-Control", "private, max-age=0, must-revalidate");
res.set("ETag", etag);
if (req.headers["if-none-match"] === etag) {
return res.status(304).end();
}
res.type("application/json").send(body);
});
app.listen(3000);
في المثال ده استخدمنا crypto.createHash من Node.js لحساب SHA-256. توثيق Node يوضح أن createHash ينشئ كائن Hash ويستخدم update وdigest لإنتاج digest نهائي.
القياس قبل وبعد
قِسها قبل ما تعتبرها نجاح. شغّل 100 طلب عادي، ثم شغّل أول طلب واحفظ قيمة ETag، وبعدها أرسل نفس القيمة في If-None-Match.
# أول طلب: خزن الـ headers
curl -i http://localhost:3000/api/products
# طلب مطابق: استخدم قيمة ETag الراجعة من أول طلب
curl -i \
-H 'If-None-Match: "PASTE_ETAG_HERE"' \
http://localhost:3000/api/products
# المتوقع: HTTP/1.1 304 Not Modified بدون JSON body
في حالة 82KB لكل رد، 100 طلب بدون ETag تساوي 8200KB تقريبًا. بعد ETag، لو 94 طلب منهم لم تتغير داتاهم، النقل الفعلي ينزل إلى حوالي 530KB. ده تحسن يقارب 93.5% في bytes المنقولة، لكنه لا يعني أن وقت الاستجابة سينخفض بنفس النسبة دائمًا.
تفاصيل لازم تنتبه لها
- لا تستخدم ETag واحد لكل المستخدمين لو الرد يحتوي صلاحيات أو أسعار مختلفة. اجعل الـ hash مبنيًا على الرد النهائي بعد تطبيق permissions.
- لا تخلط ETag مع cache عام بدون فهم. استخدم
privateلو الرد خاص بالمستخدم. - لا تحسب hash من object غير مستقر. لو ترتيب المفاتيح يتغير كل مرة، الـ ETag هيتغير بلا سبب. ثبّت ترتيب الرد أو احسبه من نسخة بيانات مستقرة.
- راقب CPU. لو الرد 5MB ويتحسب آلاف المرات في الثانية، hash كل طلب ممكن يصبح تكلفة حقيقية.
الـ trade-off بالظبط: ETag ممتاز عندما تكون تكلفة الشبكة أعلى من تكلفة حساب النسخة. لو الحساب نفسه غالي، خزّن الـ ETag بجانب الداتا أو حدّثه فقط عند تغير السجل.
متى لا تستخدم هذه الطريقة
لا تستخدمها كحل أساسي لو المشكلة الحقيقية هي query بطيئة في قاعدة البيانات؛ 304 لن ينقذك إذا كنت ما زلت تضرب DB قبل حساب الرد. لا تستخدمها أيضًا مع endpoints تتغير في كل طلب بطبيعتها، مثل أسعار لحظية أو progress stream أو feed مبني على timestamp دائم. في الحالات دي، caching قصير أو server-sent events أو pagination قد يكون أنسب.
مصادر مهمة
الخطوة التالية
افتح أكثر endpoint بيترجع بنفس الشكل في تطبيقك، وسجّل حجم 100 طلب قبل وبعد ETag. لو الفرق أقل من 30%، اتركه وانتقل لمشكلة أكبر.