قائمة تمرير تنزل إلى ٣٥fps على Android اقتصاديّ ليست "المنصّة بطيئة"، هي كومة من الأخطاء الصغيرة تستطيع إيجادها بالأدوات المشحونة في Flutter SDK. هذا المختبر يعطيك قائمة بطيئة عمداً، يمشي بك عبر ثلاث أدوات تشخيص (DevTools timeline، الـ performance overlay، ومسجّل frames صغير مخصّص)، ويطبّق أربعة إصلاحات تجلبها إلى ٥٨fps ثابتة على جهاز Snapdragon من السلسلة الرابعة.
تحليل القوائم فصل واحد. الدورة الكاملة تغطّي التخزين offline، محرّكات المزامنة، وقرارات العمارة التي تجعل تلك الـ Androids الاقتصاديّة تشعر بأنّها أصليّة.
افتح المشروع الابتدائيّ وشغّله في وضع profile على Android اقتصاديّ (Snapdragon من السلسلة الرابعة، 4GB RAM، طبقة OEM من المستوى المتوسّط). الشاشة الرئيسيّة تعرض قائمة من ٥٠٠٠ عنصر، كلّ منها بصورة، صفّان من النصّ، و chevron خلفيّ. القائمة تمرّر بـ ٣٥fps تقريباً مع إسقاط دوريّ للـ frames إلى ٢٠. هدفنا ٥٨fps ثابتة.
إن كان لديك محاكٍ فقط أو هاتف من الفئة العليا، ستبدو الأرقام مختلفة لكن شكل الإصلاحات متطابق. وضع profile غير قابل للتفاوض: إضافة وضع debug تكذب حول كلّ شيء.
سترى رسمَين بيانيَّين أعلى الشاشة. العلويّ هو GPU (raster) timeline، السفليّ هو UI (build + layout + paint) timeline. الشريط الأفقيّ على كلّ منهما ١٦.٦ms. أيّ شيء يعبر الشريط frame مُسقَط.
في مشروعنا الابتدائيّ، شريط UI ينسكب باستمرار. هذا يخبرنا أنّ العمل في build()، layout()، أو paint(). ليس الـ GPU.
الخطوة ٢: اعثر على المسار الحارّ بـ DevTools timeline#
افتح Flutter DevTools وانتقل إلى تبويبة Performance. اضغط Record، مرّر القائمة لثانيتَين، اضغط Stop. الـ flame graph يُظهر لك أيّ الطرق تأكل ميزانيّة الـ frame.
لقائمتنا الابتدائيّة، أكبر شريط هو _HomeListItem.build بـ ~٩ms لكلّ frame. ١٤ عنصراً مرئيّ، إذاً ١٤ × ٩ms = ١٢٦ms من البناء لكلّ frame، فوق الميزانيّة بكثير. الكلفة في ثلاثة أماكن:
ودجة Text تقوم بتنسيق التاريخ بنفسها على كلّ بناء.
MemoryImage يعيد فكّ تشفير نفس الـ bytes على كلّ تمرير.
هذا يُسقط كلفة التنسيق لكلّ بناء من ~٠.٤ms إلى صفر على التمريرات الحارّة. عبر العرض المرئيّ، هذا ~٥ms لكلّ frame مُستردّ.
الإصلاح ٢: خزّن الصور المفكوكة.
MemoryImage لا يخزّن افتراضيّاً؛ يخصّص ImageStream جديداً على كلّ بناء لنفس حمولة الـ bytes. أرخص إصلاح هو رفع ودجة Image إلى const أو التحوّل إلى Image.memory(... cacheWidth: 96) كي يخزّن إطار العمل بـ (bytes, cacheWidth). للصور الشبكيّة، استخدم cached_network_image.
هذا أكبر فوز فرديّ في المختبر: ~٣ms لكلّ عنصر مرئيّ مُستردّ.
الإصلاح ٣: اطوِ الـ padding المتداخل.
ثلاث ودجات Padding متداخلة تنشئ ثلاثة تمريرات layout. اطوِها في EdgeInsets.fromLTRB(...) واحد على الحاوية الخارجيّة. هذا يبدو تافهاً؛ عمق شجرة layout في قائمتنا الابتدائيّة عميق لدرجة أنّ تسطيحها يقطع ~١.٥ms لكلّ عنصر.
الإصلاح ٤: استخرج الأجزاء الثابتة إلى const.
الـ chevron الخلفيّ Icon(Icons.chevron_right) ثابت. تعليمه const يستبعده من شجرة إعادة البناء كلّيّاً. نفس الشيء لأيّ divider، spacer، أو نصّ ملصق قيمته لا تتغيّر أبداً.
أعد تسجيل الـ timeline بعد الإصلاحات. الـ flame graph لـ _HomeListItem.build يجب أن يجلس الآن حول ١.٥ms. عبر ١٤ عنصراً مرئيّاً، هذا ٢١ms المجموع، ضمن ميزانيّة ١٦.٦ms لكلّ frame بشكل مريح لأنّ الـ ١٤ عنصراً ليست جميعها يُعاد بناؤها كلّ frame؛ فقط تلك التي تمرّر إلى الرؤية.
الـ performance overlay يجب أن يُظهر الشريطَين كليهما ثابتَين تحت الخطّ. شعور تمريرك ينتقل من "خشن" إلى "هل هذا نفس التطبيق؟" عند البكسل الصحيح بالضبط.
أيّ من الإصلاحات الأربعة وحده يُعطيك تحسيناً ٢-٤fps. الأربعة معاً تُعطيك الـ ٢٣fps التي احتجتها. فخّ الانضباط هو إيجاد جانٍ واحد واضح، شحن الإصلاح، إعلان النصر، وعدم قياس الباقي أبداً. عمل الأداء ضربيّ: الإصلاحات الصغيرة تتراكم. الإصلاحات الفرديّة نادراً ما تحرّك الإبرة وحدها.
نفس الشيء صحيح لعمل محرّك المزامنة، لـ latency الـ backend، لميزانيّات البدء البارد. دورة Flutter offline-first الكاملة تُعلّم النسخة المنهجيّة من هذا: كيف تضع تنبيهات ميزانيّة الـ frame في CI، كيف تحتفظ بسجلّ perf للفريق، وكيف تُحاجج ضدّ ميزات تدفعك للعودة فوق الخطّ. هذا المختبر العضلة الأولى. الدورة خطّة التمرين.