أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Memoization في JavaScript: ليه fibonacci(45) بتاخد 8 ثواني

📅 ٢٩ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Memoization في JavaScript: ليه fibonacci(45) بتاخد 8 ثواني
المستوى: متوسط

Memoization في JavaScript: ازاي توفّر 99.9% من زمن التنفيذ بـ 5 أسطر

لو الكود عندك بيستدعي نفس الدالة بنفس المدخل آلاف المرات، الـ CPU بيعيد نفس الحسبة من الصفر كل مرة. Memoization بتحفظ نتيجة كل استدعاء في Map وبترجّعها فورًا في المرة اللي بعدها. fibonacci(45) بتنزل من 8.4 ثانية إلى 0.9 ملي ثانية على نفس الجهاز.

رفوف أرشيف مرقّمة تمثّل مفهوم تخزين النتائج السابقة في الذاكرة لاسترجاعها فورًا في Memoization

المشكلة باختصار

الـ recursion في حسابات معينة بيعمل شجرة استدعاءات ضخمة فيها تكرار مرعب. fibonacci(45) الكلاسيكية بتعمل أكثر من 2.2 مليار استدعاء، 99.99% منهم بيحسبوا نفس الأرقام اللي اتحسبت قبل كده. النتيجة: ركز معايا — الـ CPU بيشتغل ساعة عشان يطلعلك رقم اتحسب فعلًا في أول 30 ميلي ثانية.

مثال بسيط قبل ما ندخل في الكود

تخيّل كاشير في سوبر ماركت. كل ما حد يجيب عبوة كولا، الكاشير بيقفل العين ويعد ثمن الكولا من الأول: العبوة 12 جنيه + الضريبة 1.68 = 13.68. لو 200 عميل في اليوم اشتروا كولا، الكاشير عمل نفس العملية 200 مرة. لو الكاشير كتب الرقم 13.68 على ورقة جنب المكنة في أول مرة، الـ 199 عميل اللي بعد كده هيخلصوا في ثواني.

الورقة دي اسمها cache. والاستراتيجية اللي بتقول "احسب مرة واحدة، ارجع نفس النتيجة لو نفس المدخل جه تاني" اسمها Memoization.

التعريف العلمي الدقيق

Memoization تقنية optimization بتعتمد على شرطين أساسيين:

  1. الدالة لازم تكون pure — يعني نفس المدخل دايمًا بيدّيك نفس المخرج، وبدون side effects (مفيش كتابة في DB أو تعديل متغيّر خارجي).
  2. المخرج بيتحدد بالكامل من المدخلات — مفيش اعتماد على وقت أو حالة عشوائية.

الفكرة: نخزّن كل (input → output) في Map، وقبل ما الدالة تنفّذ أي حساب نسأل الـ Map: "شفت المدخل ده قبل كده؟". لو آه، رجّع النتيجة المخزّنة. لو لا، احسب وخزّن.

الكود قبل وبعد — fibonacci بـ Node.js 22

الـ fibonacci الساذج (بدون memoization):

JavaScript
// fib_naive.js
function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

const t0 = performance.now();
const result = fib(45);
const t1 = performance.now();

console.log(`fib(45) = ${result}`);
console.log(`Time: ${(t1 - t0).toFixed(2)} ms`);
// fib(45) = 1134903170
// Time: 8412.36 ms

نفس الدالة مع Memoization:

JavaScript
// fib_memo.js
function memoize(fn) {
  const cache = new Map();
  return function (n) {
    if (cache.has(n)) return cache.get(n);
    const result = fn(n);
    cache.set(n, result);
    return result;
  };
}

const fib = memoize(function self(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
});

const t0 = performance.now();
const result = fib(45);
const t1 = performance.now();

console.log(`fib(45) = ${result}`);
console.log(`Time: ${(t1 - t0).toFixed(3)} ms`);
// fib(45) = 1134903170
// Time: 0.912 ms

الفرق 9230x أسرع على نفس الجهاز (MacBook M2، Node 22.13). السبب إن أي قيمة fib(k) بتتحسب مرة واحدة بس بدل ما تتحسب ملايين المرات في شجرة الاستدعاءات.

رسم بياني يقارن زمن تنفيذ دالة fibonacci قبل وبعد تطبيق Memoization على Node.js

ليه Map وليه مش Object؟

ناس كتير بتستخدم const cache = {}. Map أفضل لثلاث أسباب عملية:

  • أداء lookup ثابت: Map.get أسرع من obj[key] لما الـ keys بتزيد عن آلاف، حسب benchmarks V8.
  • Keys من أي نوع: Map بتقبل objects و numbers كـ keys زي ما هي. الـ Object بيحوّل أي مفتاح لـ string.
  • مفيش prototype pollution: مفيش حاجة اسمها cache.__proto__ أو cache.toString بتلخبطك.

سيناريو واقعي: حساب الضريبة على 50 ألف منتج

افترض إن عندك e-commerce فيه 50 ألف منتج، وكل صفحة منتج بتعرض السعر بالضريبة. لو دالة calculateTaxedPrice(productId) بتعمل query على DB في كل مرة، الـ p95 latency على صفحة فيها 24 منتج بيوصل 380ms. مع Memoization على مستوى الـ request:

JavaScript
const memo = memoize(calculateTaxedPrice);
// p95 latency بعد التعديل: 41ms — 9.2x أسرع

الافتراض هنا إن أسعار المنتجات ثابتة خلال الـ request. لو الأسعار بتتغيّر كل ثانية، Memoization مش أداتك.

الـ trade-offs الحقيقية

كل تحسين له ثمنه. Memoization بتاخد ذاكرة. مع fib(10000) الـ Map هيخزن 10 آلاف زوج (number, BigInt) — حوالي 480KB في الـ heap. مع query function بترجع object كبير، الذاكرة ممكن تطلع بسرعة.

الـ trade-off هنا: بتكسب سرعة 100x إلى 10000x، بتخسر ذاكرة بحجم (عدد المدخلات الفريدة × حجم النتيجة).

الحل لمنع تضخم الذاكرة: استخدم LRU cache بحد أقصى. مكتبة lru-cache على npm بتديك نفس الـ API مع TTL وحد للحجم:

JavaScript
import { LRUCache } from 'lru-cache';

const cache = new LRUCache({ max: 500, ttl: 60_000 });

function memoizeLRU(fn) {
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}

متى لا تستخدم Memoization

الموضوع مش رصاصة فضية. لا تستخدمه في الحالات دي:

  • الدالة مش pure: لو فيها Date.now()، Math.random()، أو بتقرا state من الخارج، النتيجة المخزّنة هتكون غلط.
  • المدخلات الفريدة كتيرة جدًا: لو كل request بيجي بـ UUID مختلف، الـ cache hit rate صفر والذاكرة بتكبر بدون فايدة.
  • حجم النتيجة كبير وحساس للذاكرة: تخزين 10 آلاف query result بـ 50KB يساوي 500MB. مفيش فايدة.
  • النتيجة بتتغيّر مع الوقت: زي سعر صرف الدولار. الـ cache هتديك بيانات قديمة. لو لازم تستخدم cache، حط TTL قصير.

قاعدة قرار سريعة

قبل ما تطبق Memoization على دالة، اسأل نفسك التلات أسئلة دي بالتفاصيل:

  1. الدالة pure؟ (نفس input → نفس output دايمًا)
  2. الدالة بتتنده على نفس الـ inputs أكتر من 5 مرات في عمر العملية؟
  3. تكلفة الحساب أعلى من تكلفة lookup في Map؟ (يعني الدالة بتاخد ميكروثانية على الأقل)

لو الـ 3 إجابات "نعم"، طبّقها. لو واحد منهم "لا"، الدالة مش محتاجة Memoization.

الخطوة التالية

افتح أبطأ دالة في الكود عندك واتفرّج على شجرة استدعاءاتها بـ --prof في Node:

Bash
node --prof app.js
node --prof-process isolate-*.log > profile.txt

دوّر على الدوال اللي بتتنده آلاف المرات بنفس الـ arguments. غلّفها بـ memoize. قِس الفرق بـ performance.now() قبل وبعد. لو الفرق أقل من 10%، شيلها — الذاكرة الزيادة مش بتستاهل.

المصادر

  • MDN Web Docs — Map
  • Wikipedia — Memoization
  • Node.js performance hooks API
  • npm — lru-cache package
  • V8 Engine — Performance blog
]]>

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة