كواليس بناء أزرار التراجع والتقدم عندما دمرت المصفوفات أداء المتصفح

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

بيئة العمل والمتطلبات

لغة البرمجة: +Python 3.10 (بيئة محلية نظيفة).
هياكل البيانات: Lists في بايثون (كمحاكاة لعمل المصفوفات والأكداس)، بالإضافة إلى بناء كائنات مخصصة (Custom Objects).
أدوات القياس: استخدام مكتبة cProfile المدمجة لمراقبة الأداء واستهلاك الذاكرة الدقيق.
كواليس بناء أزرار التراجع والتقدم عندما دمرت المصفوفات أداء المتصفح

كنت أجلس في مكتبي، شاشة الطرفية (Terminal) تومض أمامي، والمهمة تبدو بسيطة ومباشرة: "بناء ميزة التراجع والتقدم (Back & Forward) في محرك متصفح الويب الداخلي الخاص بنا". كأي مطور يفضل الحلول السريعة في البداية، قمت بتعريف مصفوفة (Array) بسيطة لحفظ روابط الصفحات التي يزورها المستخدم. في بيئة التطوير المحلية (Localhost)، كان كل شيء يعمل بسلاسة تامة.

لكن الكارثة بدأت عندما قمنا بنشر الكود لبيئة الاختبار (Staging) وبدأنا بمحاكاة جلسات تصفح مكثفة؛ مئات التبويبات المفتوحة، وعمليات تنقل سريعة وعشوائية. فجأة، بدأت واجهة المتصفح تتجمد لأجزاء من الثانية عند كل عملية تراجع! المشكلة لم تكن في الخوادم ولا في لغة بايثون، المشكلة كانت في اختيار هيكل البيانات الخاطئ. استخدام المصفوفات لتتبع هذا النوع من البيانات أدى إلى تعقيد زمني كارثي، مما دفعني للغوص عميقاً في الذاكرة لفهم الفروق الجوهرية بين الـ Arrays، الـ Linked Lists، وصولاً إلى بطل قصتنا: الـ Stacks.

 لماذا انهارت المصفوفات؟

لكي تفهم سبب المشكلة، يجب أن تنظر إلى الذاكرة بعيون المعالج (CPU). المصفوفات (Arrays) ممتازة جداً ومحسنة للقراءة والوصول العشوائي (Random Access). عندما تطلب العنصر رقم 50، يعيده لك المعالج بتعقيد زمني O(1) لأن المصفوفة تُحجز كـ "كتلة واحدة متصلة" في الذاكرة RAM.

لكن نقطة ضعف المصفوفات القاتلة هي الإضافة والحذف في البداية أو المنتصف. تخيل أن لديك مصفوفة تحتوي على 10,000 رابط، وأردت حذف الرابط الأول للعودة للخلف. سيتعين على النظام إزاحة (Shifting) الـ 9,999 عنصراً المتبقين درجة واحدة لملء الفراغ! هذا يخلق تعقيداً زمنياً يبلغ O(n)، وهو ما كان يسبب تجمد المتصفح الخاص بنا.

القوائم المرتبطة (Linked Lists): مرونة التشتت

البديل النظري الأول الذي يتبادر للذهن لحل مشكلة الإزاحة هو القوائم المرتبطة. هنا، الذاكرة ليست متصلة. كل عنصر يُسمى (Node) يعيش في مكان عشوائي في الذاكرة، ويحتوي على البيانات بالإضافة إلى "مؤشر" (Pointer) يدلك على مكان العنصر التالي.

إليك الكود الكامل لبناء قائمة مرتبطة في بايثون:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        last_node = self.head
        while last_node.next:
            last_node = last_node.next
        last_node.next = new_node

تشريح كود القوائم المرتبطة

الكلاس Node: يمثل وحدة البناء الأساسية. عند إنشاء كائن جديد، نمرر له البيانات data (وهي الرابط في حالتنا)، ونقوم بتهيئة المؤشر next ليكون فارغاً None.
الكلاس LinkedList: يمثل الحاوية التي تدير هذه العقد. المتغير head هو النقطة المرجعية الوحيدة التي نمتلكها؛ إنه المؤشر لأول عنصر في القائمة.
الدالة append: هنا يكمن السحر والتعقيد. أولاً، نقوم بتغليف البيانات في new_node. إذا كانت القائمة فارغة (not self.head)، نجعل العقدة الجديدة هي البداية.
حلقة while: إذا لم تكن القائمة فارغة، فإننا مجبرون على "السير" عبر المؤشرات (last_node = last_node.next) بداية من الـ head وصولاً إلى آخر عقدة، ثم نقوم بربطها بالعقدة الجديدة.
تلميحة برمجية: القوائم المرتبطة أسرع من المصفوفات في الإضافة. لكن في بيئة الإنتاج الفعلية، المصفوفات غالباً أسرع! السر يكمن في مبدأ الـ "Cache Locality". المعالج (CPU) لا يجلب البيانات بايت ببايت، بل يجلب "كتلة" كاملة متجاورة ويضعها في ذاكرة الكاش السريعة جداً. لأن المصفوفات متجاورة، يجد المعالج البيانات فوراً (Cache Hit). أما القوائم المرتبطة، فبسبب تشتتها في الذاكرة، تجبر المعالج على جلب كتل جديدة باستمرار (Cache Miss)، مما يقتل الأداء الفعلي.

الحل : الأكداس (Stacks) لمحاكاة السجل

بعد استبعاد المصفوفات (بسبب مشكلة الإزاحة O(n) والقوائم المرتبطة (بسبب بطء التنقل والـ Cache Misses)، كان الحل المثالي يكمن في الأكداس (Stacks).

ميزة تصفح الويب تعتمد حرفياً على مبدأ LIFO (ما يدخل آخراً يخرج أولاً - Last In, First Out). أنت لست بحاجة للوصول للرابط رقم 50 بشكل عشوائي، أنت تتعامل دائماً مع "نهاية" القائمة. الأكداس تسمح لنا بالإضافة في النهاية (Push) والسحب من النهاية (Pop) بتعقيد زمني مثالي O(1) وبدون أي عمليات إزاحة.

إليك الكود الكامل لمحاكي المتصفح باستخدام الأكداس:

class BrowserHistory:
    def __init__(self):
        self.back_stack = []
        self.forward_stack = []
        self.current_page = "Home"

    def visit(self, url):
        self.back_stack.append(self.current_page)
        self.current_page = url
        self.forward_stack = [] 

    def back(self):
        if not self.back_stack: 
            return
        self.forward_stack.append(self.current_page)
        self.current_page = self.back_stack.pop()

    def forward(self):
        if not self.forward_stack: 
            return
        self.back_stack.append(self.current_page)
        self.current_page = self.forward_stack.pop()

تشريح كود محاكي المتصفح

دالة التهيئة __init__: قمنا بإنشاء مكدسين منفصلين باستخدام قوائم بايثون القياسية []. المكدس الأول back_stack لحفظ تاريخ ما قبل الصفحة الحالية، والمكدس الثاني forward_stack لحفظ الصفحات التي تراجعنا عنها. وقمنا بتعيين الصفحة الافتراضية Home.
الدالة visit(url): عندما يكتب المستخدم رابطاً جديداً، نقوم أولاً بدفع الصفحة الحالية current_page إلى مكدس التراجع  ()append (وهي عملية سريعة جداً O(1) لأنها تضيف في النهاية). ثم نقوم بتحديث الصفحة الحالية. الأهم هنا هو مسح مكدس التقدم بالكامل  [] = self.forward_stack ؛ لأنه عندما تفتح مساراً جديداً، تفقد القدرة على التقدم في المسار القديم.
الدالة ()back: هنا يتم تطبيق مبدأ التراجع. إذا كان مكدس التراجع فارغاً، نتوقف. إذا لم يكن كذلك، نأخذ الصفحة الحالية وندفعها إلى مكدس التقدم forward_stack تحسباً لرغبة المستخدم في العودة إليها. ثم نسحب أحدث صفحة من مكدس التراجع باستخدام الدالة ()pop ونجعلها هي الصفحة الحالية. (دالة pop في بايثون تسحب من النهاية بتعقيدO(1).
()forward: هي المرآة العكسية لدالة التراجع. نأخذ الصفحة الحالية ونضعها في مكدس التراجع، ثم نسحب أحدث صفحة من مكدس التقدم ونجعلها صفحتنا النشطة.
تنبيه تقني: رغم أننا نستخدم القوائم Lists القياسية في بايثون لتمثيل المكدس، إلا أن هذا ممارسة هندسية سليمة ومحسنة، بشرط أن تلتزم حصرياً باستخدام ()append و ()pop اللذين يعملان على "نهاية" الذاكرة المخصصة. إياك واستخدام دوال مثل insert(0, item) لدفع العناصر في البداية؛ لأنك ستدمر الأداء وتعيدنا لمشكلة الإزاحة الكارثية الأولى!

تطوير الكود 

هيكل البيانات الذي بنيناه مذهل من حيث الأداء، ولكنه يمتلك نقطة ضعف خطيرة من منظور الذاكرة. إذا تركنا المستخدم يتصفح الإنترنت لأيام متواصلة، ستنمو مكدسات back_stack و forward_stack بلا حدود. في هندسة النظم، هذا يسمى تسريب الذاكرة (Memory Leak)، وقد يؤدي لانهيار التطبيق بالكامل (Crash).

قم بتعديل BrowserHistory ليضع حداً أقصى لحجم المكدس (وليكن 50 صفحة كحد أقصى). عندما نتجاوز هذا الحد، يجب أن يتم حذف أقدم صفحة موجودة في قاع المكدس بهدوء.
تلميحة الحل والأكواد المقترحة

حاول استبدال القوائم العادية [] بكائن deque (Double Ended Queue) المتوفر في مكتبة collections المدمجة. يسمح لك هذا الكائن بتمرير معامل يُسمى maxlen؛ والذي يتولى تلقائياً مهمة إزاحة العناصر القديمة من الخلف بمجرد امتلاء الحد الأقصى بطريقة محسنة وآمنة!

الخاتمة والدروس المستفادة

الفرق الفاصل بين المبرمج الذي يكتب كوداً يعمل، والمهندس الذي يبني نظاماً يتحمل الضغط، يكمن في استيعاب "كيف تتعامل هياكل البيانات مع عتاد الجهاز". لقد تعلمنا أن المصفوفات قوية لكنها تفشل في عمليات الإزاحة، وأن القوائم المرتبطة مرنة لكنها تفشل بسبب تشتت الذاكرة، وأن الأكداس (Stacks) هي الحل المعماري الأفضل عند التعامل مع البيانات بتسلسل زمني وتراجعي. كتابة الكود هي مجرد وسيلة، لكن هندسة البيانات هي الغاية.

شاركني في التعليقات: هل قمت يوماً ببناء ميزة واكتشفت لاحقاً أن اختيارك الخاطئ لنوع البيانات (مثل Array بدلاً من Dictionary) كلفك الكثير من وقت المعالجة؟

هل واجهتك مشكلة أثناء تطبيق هذا المشروع؟

لا تبرمج بمفردك! شارك لقطة شاشة للخطأ (Screenshot) في مجتمعنا لنحلها معاً.

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