تخطى إلى المحتوى
·17 دقيقة قراءة

دوال الحافة: ما هي، ومتى تستخدمها، ومتى لا تستخدمها

بيئة تشغيل الحافة، وV8 isolates، وأسطورة البدء البارد، والتوجيه الجغرافي، واختبار A/B، والمصادقة على الحافة، ولماذا أعدت بعض الأشياء إلى Node.js. نظرة متوازنة على حوسبة الحافة.

مشاركة:X / TwitterLinkedIn

كلمة "الحافة" (edge) تُتداول كثيراً هذه الأيام. Vercel تقولها. Cloudflare تقولها. Deno تقولها. كل محاضرة في المؤتمرات عن أداء الويب تذكر حتماً "التشغيل على الحافة" وكأنها تعويذة سحرية تجعل تطبيقك سريعاً.

لقد صدّقت ذلك. نقلت middleware وroutes API، وحتى بعض منطق العرض إلى بيئة تشغيل الحافة. بعض تلك النقلات كانت ممتازة. وبعضها الآخر أعدته بهدوء إلى Node.js بعد ثلاثة أسابيع بعد تصحيح أخطاء اتصال المجمّع في الساعة الثانية صباحاً.

هذا المقال هو النسخة المتوازنة من تلك القصة — ما هي الحافة فعلاً، وأين تتألق حقاً، وأين لا تتألق مطلقاً، وكيف أقرر أي بيئة تشغيل أستخدم لكل جزء من تطبيقي.

ما هي الحافة؟#

لنبدأ بالجغرافيا. عندما يزور شخص ما موقعك الإلكتروني، ينتقل طلبه من جهازه، عبر مزود خدمة الإنترنت، عبر الإنترنت إلى خادمك، تتم معالجته، ثم يعود الرد على طول الطريق. إذا كان خادمك في us-east-1 (فيرجينيا) ومستخدمك في طوكيو، فإن تلك الرحلة ذهاباً وإياباً تغطي حوالي 14,000 كم. بسرعة الضوء عبر الألياف الضوئية، هذا حوالي 70 ميلي ثانية فقط للفيزياء — في اتجاه واحد. أضف دقة DNS، ومصافحة TLS، وأي وقت معالجة، وستنظر بسهولة إلى 200-400 ميلي ثانية قبل أن يرى مستخدمك بايتاً واحداً.

"الحافة" تعني تشغيل الكود الخاص بك على خوادم موزعة عالمياً — نفس عقد CDN التي خدمت دائماً الأصول الثابتة، لكنها الآن يمكنها أيضاً تنفيذ منطقك. بدلاً من خادم أصل واحد في فيرجينيا، يعمل الكود الخاص بك في أكثر من 300 موقع حول العالم. مستخدم في طوكيو يصل إلى خادم في طوكيو. مستخدم في باريس يصل إلى خادم في باريس.

رياضيات زمن الاستجابة بسيطة ومقنعة:

التقليدي (أصل واحد):
  طوكيو → فيرجينيا: ~140ms رحلة ذهاب وإياب (الفيزياء وحدها)
  + مصافحة TLS: ~140ms أخرى (رحلة ذهاب وإياب أخرى)
  + المعالجة: 20-50ms
  المجموع: ~300-330ms

الحافة (نقطة تواجد محلية):
  طوكيو → عقدة حافة طوكيو: ~5ms رحلة ذهاب وإياب
  + مصافحة TLS: ~5ms أخرى
  + المعالجة: 5-20ms
  المجموع: ~15-30ms

هذا تحسن بمقدار 10-20 ضعفاً للاستجابة الأولية. إنه حقيقي وقابل للقياس، ولبعض العمليات هو تحويلي.

لكن إليك ما يتجاهله التسويق: الحافة ليست بيئة خادم كاملة. إنها شيء مختلف جوهرياً.

V8 Isolates مقابل Node.js#

يعمل Node.js التقليدي في عملية نظام تشغيل كاملة. لديه وصول إلى نظام الملفات، يمكنه فتح اتصالات TCP، يمكنه إنشاء عمليات فرعية، يمكنه قراءة متغيرات البيئة كتدفق، يمكنه فعل أي شيء يمكن لعملية Linux فعله أساساً.

دوال الحافة لا تعمل على Node.js. تعمل على V8 isolates — نفس محرك JavaScript الذي يشغل Chrome، لكنه مجرد من كل شيء إلى جوهره. فكر في V8 isolate كصندوق رمل خفيف الوزن:

typescript
// هذا يعمل في Node.js لكن ليس على الحافة
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
 
const file = fs.readFileSync("/etc/hosts");        // ❌ لا يوجد نظام ملفات
const conn = createConnection({ port: 5432 });     // ❌ لا يوجد TCP خام
const result = execSync("ls -la");                  // ❌ لا توجد عمليات فرعية
process.env.DATABASE_URL;                           // ⚠️  متاح لكنه ثابت، يُعيَّن وقت النشر

ما تملكه فعلاً على الحافة هو سطح Web API — نفس الواجهات المتاحة في المتصفح:

typescript
// كل هذه تعمل على الحافة
const response = await fetch("https://api.example.com/data");
const url = new URL(request.url);
const headers = new Headers({ "Content-Type": "application/json" });
const encoder = new TextEncoder();
const encoded = encoder.encode("hello");
const hash = await crypto.subtle.digest("SHA-256", encoded);
const id = crypto.randomUUID();
 
// Web Streams API
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("chunk 1");
    controller.enqueue("chunk 2");
    controller.close();
  },
});
 
// Cache API
const cache = caches.default;
await cache.put(request, response.clone());

القيود حقيقية وصعبة:

  • الذاكرة: 128 ميجابايت لكل isolate (Cloudflare Workers)، 256 ميجابايت على بعض المنصات
  • وقت المعالج: 10-50 ميلي ثانية من وقت المعالج الفعلي (ليس وقت الساعة — await fetch() لا يُحسب، لكن JSON.parse() على حمولة 5 ميجابايت يُحسب)
  • لا وحدات أصلية: أي شيء يحتاج ربط C++ (bcrypt، sharp، canvas) مستبعد
  • لا اتصالات دائمة: لا يمكنك إبقاء اتصال قاعدة بيانات مفتوحاً بين الطلبات
  • حدود حجم الحزمة: عادة 1-5 ميجابايت للسكربت العامل بأكمله

هذا ليس Node.js على CDN. إنها بيئة تشغيل مختلفة بنموذج ذهني مختلف.

البدء البارد: الأسطورة والواقع#

ربما سمعت أن دوال الحافة لديها "صفر بدء بارد". هذا... صحيح في الغالب، والمقارنة مثيرة فعلاً.

الدالة التقليدية بدون خادم المبنية على حاويات (AWS Lambda، Google Cloud Functions) تعمل هكذا:

  1. يصل الطلب
  2. المنصة تُجهز حاوية (إذا لم تكن متاحة)
  3. الحاوية تُقلع نظام التشغيل
  4. بيئة التشغيل تُهيَّأ (Node.js، Python، إلخ.)
  5. الكود الخاص بك يُحمَّل ويُهيَّأ
  6. يتم معالجة الطلب

الخطوات 2-5 هي البدء البارد. لدالة Node.js Lambda، هذا عادة 200-500 ميلي ثانية. لدالة Java Lambda، يمكن أن يكون 2-5 ثوانٍ. لدالة .NET Lambda، 500 ميلي ثانية - 1.5 ثانية.

V8 isolates تعمل بشكل مختلف:

  1. يصل الطلب
  2. المنصة تُنشئ V8 isolate جديد (أو تُعيد استخدام واحد دافئ)
  3. الكود الخاص بك يُحمَّل (مُجمَّع مسبقاً إلى bytecode وقت النشر)
  4. يتم معالجة الطلب

الخطوتان 2-3 تأخذان أقل من 5 ميلي ثانية. غالباً أقل من 1 ميلي ثانية. الـ isolate ليس حاوية — لا يوجد نظام تشغيل للإقلاع، ولا بيئة تشغيل للتهيئة. V8 يُنشئ isolate جديد في ميكروثوانٍ. عبارة "صفر بدء بارد" هي لغة تسويقية، لكن الواقع (بدء أقل من 5 ميلي ثانية) قريب بما يكفي من الصفر بحيث لا يهم لمعظم حالات الاستخدام.

لكن إليك متى يلدغك البدء البارد على الحافة:

الحزم الكبيرة. إذا جلبت دالة الحافة الخاصة بك 2 ميجابايت من التبعيات، فذلك الكود لا يزال بحاجة للتحميل والتحليل. تعلمت هذا بالطريقة الصعبة عندما حزمت مكتبة تحقق ومكتبة تنسيق تاريخ في middleware للحافة. ارتفع البدء البارد من 2 ميلي ثانية إلى 40 ميلي ثانية. لا يزال سريعاً، لكنه ليس "صفراً".

المواقع النادرة. مزودو الحافة لديهم مئات نقاط التواجد، لكن ليس كل نقاط التواجد تبقي الكود الخاص بك دافئاً. إذا تلقيت طلباً واحداً في الساعة من نيروبي، يتم إعادة تدوير ذلك الـ isolate بين الطلبات. الطلب التالي يدفع تكلفة البدء مرة أخرى.

عدة isolates لكل طلب. إذا استدعت دالة الحافة الخاصة بك دالة حافة أخرى (أو إذا كان كل من middleware وroute API على الحافة)، فقد تُنشئ عدة isolates لطلب مستخدم واحد.

النصيحة العملية: حافظ على حزم دوال الحافة صغيرة. استورد فقط ما تحتاجه. أزل الأكواد غير المستخدمة بقوة. كلما كانت الحزمة أصغر، كان البدء البارد أسرع، وكلما صمد وعد "صفر بدء بارد".

typescript
// ❌ لا تفعل هذا على الحافة
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
 
// ✅ افعل هذا بدلاً — استخدم الواجهات المدمجة
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];

حالات الاستخدام المثالية لدوال الحافة#

بعد تجربة مكثفة، وجدت نمطاً واضحاً: دوال الحافة تتفوق عندما تحتاج لاتخاذ قرار سريع بشأن طلب قبل وصوله إلى خادم الأصل. إنها حراس بوابات، وموجّهات، ومحوّلات — وليست خوادم تطبيقات.

1. إعادة التوجيه المبنية على الموقع الجغرافي#

هذه هي حالة الاستخدام القاتلة. يصل الطلب إلى أقرب عقدة حافة، والتي تعرف بالفعل أين المستخدم. لا حاجة لاستدعاء API، ولا قاعدة بيانات بحث IP — المنصة توفر بيانات الموقع الجغرافي:

typescript
// middleware.ts — يعمل على الحافة مع كل طلب
import { NextRequest, NextResponse } from "next/server";
 
export const config = {
  matcher: ["/", "/shop/:path*"],
};
 
export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? "US";
  const city = request.geo?.city ?? "Unknown";
  const region = request.geo?.region ?? "Unknown";
 
  // إعادة التوجيه إلى المتجر الخاص بالبلد
  if (request.nextUrl.pathname === "/shop") {
    const storeMap: Record<string, string> = {
      DE: "/shop/eu",
      FR: "/shop/eu",
      GB: "/shop/uk",
      JP: "/shop/jp",
      TR: "/shop/tr",
    };
 
    const storePath = storeMap[country] ?? "/shop/us";
    if (request.nextUrl.pathname !== storePath) {
      return NextResponse.redirect(new URL(storePath, request.url));
    }
  }
 
  // إضافة رؤوس الموقع الجغرافي للاستخدام اللاحق
  const response = NextResponse.next();
  response.headers.set("x-user-country", country);
  response.headers.set("x-user-city", city);
  response.headers.set("x-user-region", region);
  return response;
}

هذا يعمل في أقل من 5 ميلي ثانية، بجوار المستخدم مباشرة. البديل — إرسال الطلب على طول الطريق إلى خادم الأصل فقط لعمل بحث IP وإعادة التوجيه — سيكلف 100-300 ميلي ثانية للمستخدمين البعيدين عن الأصل.

2. اختبار A/B بدون وميض العميل#

اختبار A/B من جانب العميل يسبب "وميض المحتوى الأصلي" المرعب — يرى المستخدم النسخة A لجزء من الثانية قبل أن يبدل JavaScript إلى النسخة B. على الحافة، يمكنك تعيين المتغير قبل أن تبدأ الصفحة حتى بالعرض:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // التحقق إذا كان المستخدم لديه تعيين متغير بالفعل
  const existingVariant = request.cookies.get("ab-variant")?.value;
 
  if (existingVariant) {
    // إعادة الكتابة إلى صفحة المتغير الصحيح
    const url = request.nextUrl.clone();
    url.pathname = `/variants/${existingVariant}${url.pathname}`;
    return NextResponse.rewrite(url);
  }
 
  // تعيين متغير جديد (تقسيم 50/50)
  const variant = Math.random() < 0.5 ? "control" : "treatment";
 
  const url = request.nextUrl.clone();
  url.pathname = `/variants/${variant}${url.pathname}`;
 
  const response = NextResponse.rewrite(url);
  response.cookies.set("ab-variant", variant, {
    maxAge: 60 * 60 * 24 * 30, // 30 يوماً
    httpOnly: true,
    sameSite: "lax",
  });
 
  return response;
}

المستخدم لا يرى أبداً وميضاً لأن إعادة الكتابة تحدث على مستوى الشبكة. المتصفح لا يعرف حتى أنه كان اختبار A/B — يستقبل فقط صفحة المتغير مباشرة.

3. التحقق من رمز المصادقة#

إذا كانت المصادقة لديك تستخدم JWT (ولا تقوم ببحث جلسات في قاعدة البيانات)، فالحافة مثالية. التحقق من JWT هو تشفير صرف — لا حاجة لقاعدة بيانات:

typescript
import { jwtVerify, importSPKI } from "jose";
import { NextRequest, NextResponse } from "next/server";
 
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
 
export async function middleware(request: NextRequest) {
  const token = request.cookies.get("session-token")?.value;
 
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  try {
    const key = await importSPKI(PUBLIC_KEY, "RS256");
    const { payload } = await jwtVerify(token, key, {
      algorithms: ["RS256"],
      issuer: "https://auth.example.com",
    });
 
    // تمرير معلومات المستخدم كرؤوس لاحقة
    const response = NextResponse.next();
    response.headers.set("x-user-id", payload.sub as string);
    response.headers.set("x-user-role", payload.role as string);
    return response;
  } catch {
    // الرمز منتهي الصلاحية أو غير صالح
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("session-token");
    return response;
  }
}

هذا النمط قوي: middleware الحافة يتحقق من الرمز ويمرر معلومات المستخدم إلى الأصل كرؤوس موثوقة. مسارات API الخاصة بك لا تحتاج للتحقق من الرمز مرة أخرى — فقط تقرأ request.headers.get("x-user-id").

4. كشف الروبوتات وتحديد المعدل#

دوال الحافة يمكنها حظر حركة المرور غير المرغوب فيها قبل أن تصل إلى الأصل أبداً:

typescript
import { NextRequest, NextResponse } from "next/server";
 
// محدد معدل بسيط في الذاكرة (لكل موقع حافة)
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
 
export function middleware(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for")?.split(",").pop()?.trim()
    ?? "unknown";
  const ua = request.headers.get("user-agent") ?? "";
 
  // حظر الروبوتات المعروفة السيئة
  const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
  if (badBots.some((bot) => ua.includes(bot))) {
    return new NextResponse("Forbidden", { status: 403 });
  }
 
  // تحديد معدل بسيط
  const now = Date.now();
  const windowMs = 60_000; // دقيقة واحدة
  const maxRequests = 100;
 
  const entry = rateLimitMap.get(ip);
  if (entry && now - entry.timestamp < windowMs) {
    entry.count++;
    if (entry.count > maxRequests) {
      return new NextResponse("Too Many Requests", {
        status: 429,
        headers: { "Retry-After": "60" },
      });
    }
  } else {
    rateLimitMap.set(ip, { count: 1, timestamp: now });
  }
 
  // تنظيف دوري لمنع تسرب الذاكرة
  if (rateLimitMap.size > 10_000) {
    const cutoff = now - windowMs;
    for (const [key, val] of rateLimitMap) {
      if (val.timestamp < cutoff) rateLimitMap.delete(key);
    }
  }
 
  return NextResponse.next();
}

تحذير واحد: خريطة تحديد المعدل أعلاه هي لكل isolate ولكل موقع. إذا كان لديك 300 موقع حافة، فكل واحد لديه خريطته الخاصة. لتحديد معدل صارم، تحتاج مخزناً موزعاً مثل Upstash Redis أو Cloudflare Durable Objects. لكن لمنع إساءة الاستخدام التقريبية، حدود كل موقع تعمل بشكل جيد بشكل مدهش.

5. إعادة كتابة الطلبات ورؤوس التخصيص#

دوال الحافة ممتازة في تحويل الطلبات قبل وصولها إلى الأصل:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const url = request.nextUrl;
 
  // تفاوض المحتوى حسب الجهاز
  const ua = request.headers.get("user-agent") ?? "";
  const isMobile = /mobile|android|iphone/i.test(ua);
  response.headers.set("x-device-type", isMobile ? "mobile" : "desktop");
 
  // أعلام الميزات من الكعكة
  const flags = request.cookies.get("feature-flags")?.value;
  if (flags) {
    response.headers.set("x-feature-flags", flags);
  }
 
  // كشف اللغة للتدويل
  const acceptLanguage = request.headers.get("accept-language") ?? "en";
  const preferredLocale = acceptLanguage.split(",")[0]?.split("-")[0] ?? "en";
  const supportedLocales = [
    "en", "tr", "de", "fr", "es", "pt", "ja", "ko", "it",
    "nl", "ru", "pl", "uk", "sv", "cs", "ar", "hi", "zh",
  ];
  const locale = supportedLocales.includes(preferredLocale)
    ? preferredLocale
    : "en";
 
  if (!url.pathname.startsWith(`/${locale}`) && !url.pathname.startsWith("/api")) {
    return NextResponse.redirect(new URL(`/${locale}${url.pathname}`, request.url));
  }
 
  return response;
}

أين تفشل الحافة#

هذا هو القسم الذي تتجاهله صفحات التسويق. لقد اصطدمت بكل واحد من هذه الجدران.

1. اتصالات قاعدة البيانات#

هذه هي المشكلة الكبرى. قواعد البيانات التقليدية (PostgreSQL، MySQL) تستخدم اتصالات TCP دائمة. خادم Node.js يفتح مجمّع اتصالات عند البدء ويعيد استخدام تلك الاتصالات عبر الطلبات. فعال، مُثبت، ومفهوم جيداً.

دوال الحافة لا تستطيع فعل هذا. كل isolate مؤقت. لا توجد مرحلة "بدء" تفتح فيها الاتصالات. حتى لو استطعت فتح اتصال، قد يتم إعادة تدوير الـ isolate بعد طلب واحد، مما يُضيع وقت إعداد الاتصال.

typescript
// ❌ هذا النمط لا يعمل جوهرياً على الحافة
import { Pool } from "pg";
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // مجمّع اتصالات من 10
});
 
// كل استدعاء حافة سيقوم بـ:
// 1. إنشاء Pool جديد (لا يمكن إعادة الاستخدام عبر الاستدعاءات بشكل موثوق)
// 2. فتح اتصال TCP إلى قاعدة البيانات (التي في us-east-1، وليس على الحافة)
// 3. مصافحة TLS مع قاعدة البيانات
// 4. تشغيل الاستعلام
// 5. تجاهل الاتصال عند إعادة تدوير الـ isolate
 
// حتى مع خدمات تجميع الاتصالات مثل PgBouncer،
// لا تزال تدفع زمن الشبكة من الحافة → قاعدة بيانات الأصل

مشكلة رحلة قاعدة البيانات أساسية. قاعدة بياناتك في منطقة واحدة. دالة الحافة في 300 منطقة. كل استعلام قاعدة بيانات من الحافة يجب أن ينتقل من موقع الحافة إلى منطقة قاعدة البيانات والعودة. لمستخدم في طوكيو يصل إلى عقدة حافة طوكيو، لكن قاعدة بياناتك في فيرجينيا:

دالة الحافة في طوكيو
  → استعلام إلى PostgreSQL في فيرجينيا: ~140ms رحلة ذهاب وإياب
  → استعلام ثانٍ: ~140ms أخرى
  → المجموع: 280ms لاستعلامين فقط

دالة Node.js في فيرجينيا (نفس منطقة قاعدة البيانات)
  → استعلام إلى PostgreSQL: ~1ms رحلة ذهاب وإياب
  → استعلام ثانٍ: ~1ms أخرى
  → المجموع: 2ms لاستعلامين

دالة الحافة أبطأ بـ 140 ضعفاً لعمليات قاعدة البيانات في هذا السيناريو. لا يهم أن دالة الحافة بدأت أسرع — رحلات قاعدة البيانات تهيمن على كل شيء.

لهذا السبب توجد وسطاء قواعد بيانات عبر HTTP (مشغل Neon بدون خادم، مشغل PlanetScale المبني على fetch، واجهة REST لـ Supabase). تعمل، لكنها لا تزال ترسل طلبات HTTP إلى قاعدة بيانات في منطقة واحدة. تحل مشكلة "لا يمكن استخدام TCP" لكن ليس مشكلة "قاعدة البيانات بعيدة".

typescript
// ✅ هذا يعمل على الحافة (وصول قاعدة بيانات عبر HTTP)
// لكنه لا يزال بطيئاً إذا كانت قاعدة البيانات بعيدة عن عقدة الحافة
import { neon } from "@neondatabase/serverless";
 
export const runtime = "edge";
 
export async function GET(request: Request) {
  const sql = neon(process.env.DATABASE_URL!);
  // هذا يرسل طلب HTTP إلى قاعدة بيانات Neon
  // يعمل، لكن زمن الاستجابة يعتمد على المسافة إلى منطقة قاعدة البيانات
  const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
  return Response.json(posts);
}

2. المهام طويلة التشغيل#

دوال الحافة لديها حدود وقت المعالج، عادة 10-50 ميلي ثانية من وقت الحوسبة الفعلي. وقت الساعة أكثر سخاءً (عادة 30 ثانية)، لكن العمليات كثيفة المعالج ستصل إلى الحد بسرعة:

typescript
// ❌ هذه ستتجاوز حدود وقت المعالج على الحافة
export const runtime = "edge";
 
export async function POST(request: Request) {
  const data = await request.json();
 
  // معالجة الصور — كثيفة المعالج
  // (أيضاً لا يمكن استخدام sharp لأنها وحدة أصلية)
  const processed = heavyImageProcessing(data.image);
 
  // إنشاء PDF — كثيف المعالج + يحتاج واجهات Node.js
  const pdf = generatePDF(data.content);
 
  // تحويل بيانات كبيرة
  const result = data.items // 100,000 عنصر
    .map(transform)
    .filter(validate)
    .sort(compare)
    .reduce(aggregate, {});
 
  return Response.json(result);
}

إذا كانت دالتك تحتاج أكثر من بضع ميلي ثوانٍ من وقت المعالج، فمكانها على خادم Node.js إقليمي. نقطة.

3. التبعيات الخاصة بـ Node.js فقط#

هذه تفاجئ الناس. عدد مدهش من حزم npm يعتمد على وحدات Node.js المدمجة:

typescript
// ❌ هذه الحزم لن تعمل على الحافة
import bcrypt from "bcrypt";            // ربط C++ أصلي
import sharp from "sharp";              // ربط C++ أصلي
import puppeteer from "puppeteer";      // يحتاج نظام الملفات + child_process
import nodemailer from "nodemailer";    // يحتاج وحدة net
import { readFile } from "fs/promises"; // واجهة نظام ملفات Node.js
import mongoose from "mongoose";         // اتصالات TCP + واجهات Node.js
 
// ✅ بدائل متوافقة مع الحافة
import { hashSync } from "bcryptjs";    // تنفيذ JavaScript صرف (أبطأ)
// للصور: استخدم خدمة أو API منفصلة
// للبريد الإلكتروني: استخدم API بريد إلكتروني عبر HTTP (Resend، SendGrid REST)
// لقاعدة البيانات: استخدم عملاء عبر HTTP

قبل نقل أي شيء إلى الحافة، تحقق من كل تبعية. require("fs") واحد مدفون ثلاثة مستويات عميقاً في شجرة التبعيات سيُحطم دالة الحافة في وقت التشغيل — وليس وقت البناء. ستنشر، كل شيء يبدو جيداً، ثم أول طلب يصل إلى ذلك المسار وتحصل على خطأ غامض.

4. أحجام الحزم الكبيرة#

منصات الحافة لديها حدود صارمة لحجم الحزمة:

  • Cloudflare Workers: 1 ميجابايت (مجاني)، 5 ميجابايت (مدفوع)
  • Vercel Edge Functions: 4 ميجابايت (مضغوط)
  • Deno Deploy: 20 ميجابايت

يبدو هذا كافياً حتى تستورد مكتبة مكونات واجهة مستخدم، ومكتبة تحقق، ومكتبة تاريخ. مرة كان لدي middleware حافة تضخم إلى 3.5 ميجابايت لأنني استوردت من ملف barrel سحب دليل @/components بأكمله.

typescript
// ❌ استيرادات ملف barrel يمكن أن تسحب الكثير جداً
import { validateEmail } from "@/lib/utils";
// إذا كان utils.ts يعيد التصدير من 20 وحدة أخرى، كلها يتم تحزيمها
 
// ✅ استورد مباشرة من المصدر
import { validateEmail } from "@/lib/validators/email";

5. البث و WebSockets#

دوال الحافة يمكنها عمل استجابات متدفقة (Web Streams API)، لكن اتصالات WebSocket طويلة العمر قصة مختلفة. بينما بعض المنصات تدعم WebSockets على الحافة (Cloudflare Workers، Deno Deploy)، الطبيعة المؤقتة لدوال الحافة تجعلها غير مناسبة للاتصالات ذات الحالة وطويلة العمر.

بيئة تشغيل الحافة في Next.js#

يجعل Next.js من السهل الاختيار لبيئة تشغيل الحافة على أساس كل مسار. لا تحتاج للالتزام بالكامل — تختار بالضبط أي المسارات تعمل على الحافة.

Middleware (دائماً على الحافة)#

middleware في Next.js يعمل دائماً على الحافة. هذا بالتصميم — middleware يعترض كل طلب مطابق، لذا يحتاج أن يكون سريعاً وموزعاً عالمياً:

typescript
// middleware.ts — يعمل دائماً على الحافة، لا حاجة للاشتراك
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // هذا يعمل قبل كل طلب مطابق
  // أبقه سريعاً — لا استدعاءات قاعدة بيانات، لا حوسبة ثقيلة
  return NextResponse.next();
}
 
export const config = {
  // التشغيل فقط على مسارات محددة
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
  ],
};

مسارات API على الحافة#

أي معالج مسار يمكنه الاشتراك في بيئة تشغيل الحافة:

typescript
// app/api/hello/route.ts
export const runtime = "edge"; // هذا السطر الواحد يغير بيئة التشغيل
 
export async function GET(request: Request) {
  return Response.json({
    message: "Hello from the edge",
    region: process.env.VERCEL_REGION ?? "unknown",
    timestamp: Date.now(),
  });
}

مسارات الصفحات على الحافة#

حتى الصفحات الكاملة يمكنها العرض على الحافة، رغم أنني أفكر ملياً قبل فعل ذلك:

typescript
// app/dashboard/page.tsx
export const runtime = "edge";
 
export default async function DashboardPage() {
  // تذكر: لا واجهات Node.js هنا
  // أي جلب بيانات يجب أن يستخدم fetch() أو عملاء متوافقين مع الحافة
  const data = await fetch("https://api.example.com/dashboard", {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
    next: { revalidate: 60 },
  }).then((r) => r.json());
 
  return (
    <main>
      <h1>لوحة التحكم</h1>
      {/* عرض البيانات */}
    </main>
  );
}

ما هو المتاح في بيئة تشغيل الحافة#

إليك مرجعاً عملياً لما يمكنك ولا يمكنك استخدامه:

typescript
// ✅ متاح على الحافة
fetch()                          // طلبات HTTP
Request / Response               // طلب/استجابة معيار الويب
Headers                          // رؤوس HTTP
URL / URLSearchParams            // تحليل URL
TextEncoder / TextDecoder        // ترميز النصوص
crypto.subtle                    // عمليات التشفير (التوقيع، التجزئة)
crypto.randomUUID()              // توليد UUID
crypto.getRandomValues()         // أرقام عشوائية تشفيرية
structuredClone()                // نسخ عميق
atob() / btoa()                  // ترميز/فك ترميز Base64
setTimeout() / setInterval()     // المؤقتات (لكن تذكر حدود المعالج)
console.log()                    // التسجيل
ReadableStream / WritableStream  // البث
AbortController / AbortSignal    // إلغاء الطلب
URLPattern                       // مطابقة أنماط URL
 
// ❌ غير متاح على الحافة
require()                        // CommonJS (استخدم import)
fs / path / os                   // وحدات Node.js المدمجة
process.exit()                   // التحكم في العملية
Buffer                           // استخدم Uint8Array بدلاً
__dirname / __filename           // استخدم import.meta.url
setImmediate()                   // ليس معيار ويب

المصادقة على الحافة: النمط الكامل#

أريد التعمق أكثر في المصادقة لأنها واحدة من أكثر حالات استخدام الحافة تأثيراً، لكنها أيضاً سهلة الخطأ.

النمط الذي يعمل هو: التحقق من الرمز على الحافة، تمرير المطالبات الموثوقة لاحقاً، عدم لمس قاعدة البيانات أبداً في middleware.

typescript
// lib/edge-auth.ts — أدوات مصادقة متوافقة مع الحافة
import { jwtVerify, SignJWT, importSPKI, importPKCS8 } from "jose";
 
const PUBLIC_KEY_PEM = process.env.JWT_PUBLIC_KEY!;
const ISSUER = "https://auth.myapp.com";
const AUDIENCE = "https://myapp.com";
 
export interface TokenPayload {
  sub: string;
  email: string;
  role: "user" | "admin" | "moderator";
  iat: number;
  exp: number;
}
 
export async function verifyToken(token: string): Promise<TokenPayload | null> {
  try {
    const publicKey = await importSPKI(PUBLIC_KEY_PEM, "RS256");
    const { payload } = await jwtVerify(token, publicKey, {
      algorithms: ["RS256"],
      issuer: ISSUER,
      audience: AUDIENCE,
      clockTolerance: 30, // 30 ثانية تسامح انحراف الساعة
    });
 
    return payload as unknown as TokenPayload;
  } catch {
    return null;
  }
}
 
export function isTokenExpiringSoon(payload: TokenPayload): boolean {
  const now = Math.floor(Date.now() / 1000);
  const fiveMinutes = 5 * 60;
  return payload.exp - now < fiveMinutes;
}
typescript
// middleware.ts — middleware المصادقة
import { NextRequest, NextResponse } from "next/server";
import { verifyToken, isTokenExpiringSoon } from "./lib/edge-auth";
 
const PUBLIC_PATHS = ["/", "/login", "/register", "/api/auth/login"];
const ADMIN_PATHS = ["/admin"];
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // تخطي المصادقة للمسارات العامة
  if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
    return NextResponse.next();
  }
 
  // استخراج الرمز
  const token = request.cookies.get("auth-token")?.value;
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  // التحقق من الرمز (تشفير صرف — لا استدعاء قاعدة بيانات)
  const payload = await verifyToken(token);
  if (!payload) {
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("auth-token");
    return response;
  }
 
  // التحكم في الوصول المبني على الأدوار
  if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }
 
  // تمرير معلومات المستخدم المُتحقق منها إلى الأصل كرؤوس موثوقة
  const response = NextResponse.next();
  response.headers.set("x-user-id", payload.sub);
  response.headers.set("x-user-email", payload.email);
  response.headers.set("x-user-role", payload.role);
 
  // الإشارة إذا كان الرمز يحتاج تجديد
  if (isTokenExpiringSoon(payload)) {
    response.headers.set("x-token-refresh", "true");
  }
 
  return response;
}
typescript
// app/api/profile/route.ts — خادم الأصل يقرأ الرؤوس الموثوقة
export async function GET(request: Request) {
  // هذه الرؤوس عُيّنت بواسطة middleware الحافة بعد التحقق من JWT
  // هي موثوقة لأنها تأتي من بنيتنا التحتية
  const userId = request.headers.get("x-user-id");
  const userRole = request.headers.get("x-user-role");
 
  if (!userId) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  // الآن يمكننا الوصول لقاعدة البيانات — نحن على خادم الأصل،
  // بجوار قاعدة البيانات مباشرة، مع مجمّع اتصالات
  const user = await db.user.findUnique({ where: { id: userId } });
 
  return Response.json(user);
}

الرؤية الأساسية: الحافة تقوم بالجزء السريع (التحقق من التشفير)، والأصل يقوم بالجزء البطيء (استعلامات قاعدة البيانات). كل منهما يعمل حيث يكون الأكثر كفاءة.

تحذير مهم واحد: هذا يعمل فقط مع JWT. إذا كان نظام المصادقة لديك يتطلب بحث قاعدة بيانات في كل طلب (مثل المصادقة المبنية على الجلسات بكعكة معرّف الجلسة)، فالحافة لا تستطيع المساعدة — ستظل بحاجة لاستدعاء قاعدة البيانات، مما يعني رحلة ذهاب وإياب إلى منطقة الأصل.

التخزين المؤقت على الحافة#

التخزين المؤقت على الحافة هو حيث تصبح الأمور مثيرة. عقد الحافة يمكنها تخزين الاستجابات مؤقتاً، مما يعني أن الطلبات اللاحقة لنفس URL تُقدَّم مباشرة من الحافة دون الوصول إلى الأصل على الإطلاق.

Cache-Control بالشكل الصحيح#

typescript
// app/api/products/route.ts
export const runtime = "edge";
 
export async function GET(request: Request) {
  const url = new URL(request.url);
  const category = url.searchParams.get("category") ?? "all";
 
  const products = await fetch(
    `${process.env.ORIGIN_API}/products?category=${category}`,
  ).then((r) => r.json());
 
  return Response.json(products, {
    headers: {
      // التخزين المؤقت على CDN لـ 60 ثانية
      // تقديم القديم أثناء إعادة التحقق لمدة تصل إلى 5 دقائق
      // العميل يمكنه التخزين المؤقت لـ 10 ثوانٍ
      "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
 
      // التنويع حسب هذه الرؤوس بحيث تحصل المتغيرات المختلفة على مداخل تخزين مؤقت مختلفة
      Vary: "Accept-Language, Accept-Encoding",
 
      // علامة تخزين مؤقت خاصة بـ CDN للإبطال المستهدف
      "Cache-Tag": `products,category-${category}`,
    },
  });
}

نمط stale-while-revalidate قوي بشكل خاص على الحافة. إليك ما يحدث:

  1. الطلب الأول: الحافة تجلب من الأصل، تخزن الاستجابة مؤقتاً، تُرجعها
  2. الطلبات خلال 60 ثانية: الحافة تقدم من التخزين المؤقت (0ms زمن أصل)
  3. طلب في 61-360 ثانية: الحافة تقدم النسخة المخزنة القديمة فوراً، لكنها تجلب نسخة جديدة من الأصل في الخلفية
  4. بعد 360 ثانية: التخزين المؤقت منتهي تماماً، الطلب التالي يذهب إلى الأصل

مستخدموك يحصلون تقريباً دائماً على استجابة مخزنة مؤقتاً. مقايضة الحداثة واضحة وقابلة للضبط.

Edge Config للتكوين الديناميكي#

Edge Config من Vercel (وخدمات مماثلة من منصات أخرى) تتيح لك تخزين تكوين مفتاح-قيمة يتم نسخه إلى كل موقع حافة. هذا مفيد بشكل لا يُصدق لأعلام الميزات، وقواعد إعادة التوجيه، وتكوين اختبار A/B التي تريد تحديثها بدون إعادة النشر:

typescript
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
 
export async function middleware(request: NextRequest) {
  // قراءات Edge Config سريعة للغاية (~1ms) لأن
  // البيانات تُنسخ إلى كل موقع حافة
  const maintenanceMode = await get<boolean>("maintenance_mode");
 
  if (maintenanceMode) {
    return NextResponse.rewrite(new URL("/maintenance", request.url));
  }
 
  // أعلام الميزات
  const features = await get<Record<string, boolean>>("feature_flags");
  if (features?.["new_pricing_page"] && request.nextUrl.pathname === "/pricing") {
    return NextResponse.rewrite(new URL("/pricing-v2", request.url));
  }
 
  // إعادات توجيه ديناميكية (تحديث إعادات التوجيه بدون إعادة نشر)
  const redirects = await get<Array<{ from: string; to: string; permanent: boolean }>>(
    "redirects",
  );
 
  if (redirects) {
    const match = redirects.find((r) => r.from === request.nextUrl.pathname);
    if (match) {
      return NextResponse.redirect(
        new URL(match.to, request.url),
        match.permanent ? 308 : 307,
      );
    }
  }
 
  return NextResponse.next();
}

هذا مغير للعبة حقيقي. قبل Edge Config، تغيير علم ميزة كان يعني تغيير في الكود وإعادة نشر. الآن تُحدّث قيمة JSON في لوحة تحكم وتنتشر عالمياً في ثوانٍ.

رياضيات الأداء الحقيقية#

لنقم بالرياضيات الصادقة بدلاً من رياضيات التسويق. سأقارن ثلاث معماريات لنقطة نهاية API نموذجية تحتاج لاستعلام قاعدة بيانات:

السيناريو: API ملف المستخدم (استعلامان قاعدة بيانات)#

المعمارية أ: Node.js إقليمي تقليدي

مستخدم في طوكيو → أصل في فيرجينيا: 140ms
  + استعلام قاعدة بيانات 1 (نفس المنطقة): 2ms
  + استعلام قاعدة بيانات 2 (نفس المنطقة): 2ms
  + المعالجة: 5ms
  = المجموع: ~149ms

المعمارية ب: دالة حافة مع قاعدة بيانات HTTP

مستخدم في طوكيو → حافة في طوكيو: 5ms
  + استعلام قاعدة بيانات 1 (HTTP إلى فيرجينيا): 145ms
  + استعلام قاعدة بيانات 2 (HTTP إلى فيرجينيا): 145ms
  + المعالجة: 3ms
  = المجموع: ~298ms ← أبطأ من الإقليمي

المعمارية ج: دالة حافة مع قاعدة بيانات إقليمية (نسخة قراءة)

مستخدم في طوكيو → حافة في طوكيو: 5ms
  + استعلام قاعدة بيانات 1 (HTTP إلى نسخة طوكيو): 8ms
  + استعلام قاعدة بيانات 2 (HTTP إلى نسخة طوكيو): 8ms
  + المعالجة: 3ms
  = المجموع: ~24ms ← الأسرع، لكن يتطلب قاعدة بيانات متعددة المناطق

المعمارية د: الحافة للمصادقة + إقليمي للبيانات

مستخدم في طوكيو → middleware حافة في طوكيو: 5ms (التحقق من JWT)
  → أصل في فيرجينيا: 140ms
  + استعلام قاعدة بيانات 1 (نفس المنطقة): 2ms
  + استعلام قاعدة بيانات 2 (نفس المنطقة): 2ms
  + المعالجة: 5ms
  = المجموع: ~154ms
  (لكن المصادقة تمت بالفعل — الأصل لا يحتاج للتحقق مرة أخرى)
  (والطلبات غير المصرح بها تُحظر على الحافة — لا تصل أبداً إلى الأصل)

الخلاصات:

  1. الحافة + قاعدة بيانات الأصل = غالباً أبطأ من مجرد استخدام خادم إقليمي
  2. الحافة + قاعدة بيانات متعددة المناطق = الأسرع لكن الأكثر تكلفة وتعقيداً
  3. الحافة للحراسة + إقليمي للبيانات = أفضل توازن عملي
  4. حافة صرفة (بدون قاعدة بيانات) = لا تُقهر لأشياء مثل إعادات التوجيه وفحوصات المصادقة

المعمارية د هي ما أستخدمه لمعظم المشاريع. الحافة تتعامل مع ما تجيده (القرارات السريعة، المصادقة، التوجيه)، وخادم Node.js الإقليمي يتعامل مع ما يجيده (استعلامات قاعدة البيانات، الحوسبة الثقيلة).

متى تفوز الحافة فعلاً: العمليات بدون قاعدة بيانات#

الرياضيات تنقلب تماماً عندما لا تكون هناك قاعدة بيانات:

إعادة التوجيه (حافة):
  مستخدم في طوكيو → حافة في طوكيو → استجابة إعادة توجيه: ~5ms

إعادة التوجيه (إقليمي):
  مستخدم في طوكيو → أصل في فيرجينيا → استجابة إعادة توجيه: ~280ms
استجابة API ثابتة (حافة + تخزين مؤقت):
  مستخدم في طوكيو → حافة في طوكيو → استجابة مخزنة مؤقتاً: ~5ms

استجابة API ثابتة (إقليمي):
  مستخدم في طوكيو → أصل في فيرجينيا → استجابة: ~280ms
حظر الروبوتات (حافة):
  روبوت سيئ في أي مكان → حافة (الأقرب) → استجابة 403: ~5ms
  (الروبوت لا يصل أبداً إلى خادم الأصل)

حظر الروبوتات (إقليمي):
  روبوت سيئ في أي مكان → أصل في فيرجينيا → استجابة 403: ~280ms
  (الروبوت لا يزال يستهلك موارد الأصل)

للعمليات التي لا تحتاج قاعدة بيانات، الحافة أسرع بـ 20-50 ضعفاً. هذا ليس تسويقاً — إنها فيزياء.

إطار عمل قراراتي#

بعد سنة من العمل مع دوال الحافة في الإنتاج، إليك المخطط الذي أستخدمه لكل نقطة نهاية جديدة أو جزء من المنطق:

الخطوة 1: هل يحتاج واجهات Node.js؟#

إذا استورد fs، net، child_process، أو أي وحدة أصلية — Node.js إقليمي. لا نقاش.

الخطوة 2: هل يحتاج استعلامات قاعدة بيانات؟#

إذا نعم، وليس لديك نسخ قراءة قريبة من مستخدميك — Node.js إقليمي (في نفس منطقة قاعدة البيانات). رحلات قاعدة البيانات ستهيمن.

إذا نعم، ولديك نسخ قراءة موزعة عالمياً — الحافة يمكن أن تعمل، باستخدام عملاء قاعدة بيانات عبر HTTP.

الخطوة 3: هل هو قرار بشأن طلب (توجيه، مصادقة، إعادة توجيه)؟#

إذا نعم — الحافة. هذه هي النقطة الجميلة. أنت تتخذ قراراً سريعاً يحدد ما يحدث للطلب قبل وصوله إلى الأصل.

الخطوة 4: هل الاستجابة قابلة للتخزين المؤقت؟#

إذا نعم — الحافة مع رؤوس Cache-Control مناسبة. حتى لو ذهب الطلب الأول إلى الأصل، الطلبات اللاحقة تُقدَّم من تخزين الحافة المؤقت.

الخطوة 5: هل هو كثيف المعالج؟#

إذا تضمن حوسبة كبيرة (معالجة صور، إنشاء PDF، تحويلات بيانات كبيرة) — Node.js إقليمي.

الخطوة 6: ما مدى حساسية زمن الاستجابة؟#

إذا كانت مهمة خلفية أو webhook — Node.js إقليمي. لا أحد ينتظرها. إذا كان طلباً يواجه المستخدم حيث كل ميلي ثانية مهمة — الحافة، إذا استوفت المعايير الأخرى.

ورقة الغش#

typescript
// ✅ مثالي للحافة
// - Middleware (مصادقة، إعادات توجيه، إعادات كتابة، رؤوس)
// - منطق الموقع الجغرافي
// - تعيين اختبار A/B
// - كشف الروبوتات / قواعد WAF
// - استجابات API صديقة التخزين المؤقت
// - فحوصات أعلام الميزات
// - استجابات CORS المسبقة
// - تحويلات بيانات ثابتة (بدون قاعدة بيانات)
// - التحقق من توقيع Webhook
 
// ❌ أبقِ على Node.js إقليمي
// - عمليات CRUD لقاعدة البيانات
// - رفع / معالجة الملفات
// - معالجة الصور
// - إنشاء PDF
// - إرسال البريد الإلكتروني (استخدم API عبر HTTP، لكن لا يزال إقليمي)
// - خوادم WebSocket
// - المهام الخلفية / الطوابير
// - أي شيء يستخدم حزم npm أصلية
// - صفحات SSR مع استعلامات قاعدة بيانات
// - محللات GraphQL التي تستعلم قواعد البيانات
 
// 🤔 يعتمد
// - المصادقة (حافة لـ JWT، إقليمي لجلسة قاعدة البيانات)
// - مسارات API (حافة إذا بدون قاعدة بيانات، إقليمي إذا مع قاعدة بيانات)
// - الصفحات المُعرَضة من الخادم (حافة إذا البيانات من تخزين مؤقت/fetch، إقليمي إذا قاعدة بيانات)
// - الميزات الفورية (حافة للمصادقة الأولية، إقليمي للاتصالات الدائمة)

ما أشغّله فعلاً على الحافة#

لهذا الموقع، إليك التوزيع:

الحافة (middleware):

  • كشف اللغة وإعادة التوجيه
  • تصفية الروبوتات
  • رؤوس الأمان (CSP، HSTS، إلخ.)
  • تسجيل الوصول
  • تحديد المعدل (أساسي)

Node.js إقليمي:

  • عرض محتوى المدونة (معالجة MDX تحتاج واجهات Node.js عبر Velite)
  • مسارات API التي تتعامل مع Redis
  • إنشاء صور OG (يحتاج وقت معالج أكثر)
  • إنشاء خلاصة RSS

ثابت (بدون بيئة تشغيل إطلاقاً):

  • صفحات الأدوات (مُعرَضة مسبقاً وقت البناء)
  • صفحات مقالات المدونة (مُعرَضة مسبقاً وقت البناء)
  • جميع الصور والأصول (مقدَّمة من CDN)

أفضل بيئة تشغيل هي غالباً بدون بيئة تشغيل. إذا استطعت عرض شيء مسبقاً وقت البناء وتقديمه كأصل ثابت، فسيكون ذلك دائماً أسرع من أي دالة حافة. الحافة هي للأشياء التي تحتاج فعلاً أن تكون ديناميكية مع كل طلب.

الملخص الصادق#

دوال الحافة ليست بديلاً للخوادم التقليدية. إنها مكمّل. إنها أداة إضافية في صندوق أدوات المعمارية الخاص بك — واحدة قوية بشكل لا يُصدق لحالات الاستخدام الصحيحة وضارة بنشاط للخاطئة منها.

القاعدة الأساسية التي أعود إليها دائماً: إذا كانت دالتك تحتاج للوصول إلى قاعدة بيانات في منطقة واحدة، فوضع الدالة على الحافة لا يساعد — بل يضر. لقد أضفت قفزة. الدالة تعمل أسرع، لكنها تقضي 100+ ميلي ثانية في العودة إلى قاعدة البيانات. النتيجة الصافية: أبطأ من تشغيل كل شيء في منطقة واحدة.

لكن للقرارات التي يمكن اتخاذها فقط من المعلومات في الطلب نفسه — الموقع الجغرافي، الكعكات، الرؤوس، JWT — الحافة لا تُقهر. تلك الاستجابات في 5 ميلي ثانية ليست مقاييس اصطناعية. إنها حقيقية، ومستخدموك يشعرون بالفرق.

لا تنقل كل شيء إلى الحافة. ولا تُبعد كل شيء عن الحافة. ضع كل جزء من المنطق حيث تفضله الفيزياء.

مقالات ذات صلة