تحسين الأداء

اكتشاف التقطع والقضاء عليه

16 دقيقة الدرس 9 من 12

اكتشاف التقطع والقضاء عليه

تتطلب الرسوم المتحركة السلسة في Flutter عرض كل إطار في أقل من 16 ميلي ثانية — الميزانية الزمنية لشاشة تعمل بـ 60 إطاراً في الثانية. أي إطار يستغرق وقتاً أطول يتسبب في اهتزاز مرئي يُعرف بـ التقطع (jank). يلاحظ المستخدمون التقطع فوراً: يبدو التمرير بطيئاً، وتتعثر الرسوم المتحركة، وتبدو التفاعلات غير مستجيبة. تعلمك هذه الدرس ما هو التقطع على مستوى المحرك، وكيفية إعادة إنتاجه بشكل موثوق، وكيفية تتبع سببه الجذري باستخدام Flutter DevTools.

ما هو التقطع؟

تعالج خط أنابيب العرض في Flutter كل إطار في خيطين متوازيين: خيط واجهة المستخدم (UI thread) (عزل Dart — يبني شجرة الودجات والتخطيط) وخيط الرسم النقطي (raster thread) (وحدة معالجة الرسوميات — يرسم وحدات البكسل). يتشارك كلا الخيطين ميزانية الإطار البالغة 16 مللي ثانية. إذا فات أي خيط موعده النهائي، لا يستطيع محول GPU تبادل المخزن المؤقت في الوقت المناسب وتكرر الشاشة الإطار السابق، مما ينتج إطاراً مسقطاً مرئياً.

  • تقطع خيط واجهة المستخدم — ناجم عن عمل متزامن ثقيل في build() أو setState() أو حساب التخطيط.
  • تقطع خيط الرسم النقطي — ناجم عن عمليات GPU مكلفة كتجميع الشيدر، أو استدعاءات saveLayer() الكبيرة، أو تركيب ClipPath المعقد.
  • تقطع خيط المنصة — ناجم عن عمل مكلف على قناة المنصة (مثل استدعاءات الإضافات التي تحجب الخيط الرئيسي).
ملاحظة: يستهدف Flutter معدل 60 إطاراً في الثانية (16 مللي ثانية/إطار) على معظم الأجهزة و120 إطاراً في الثانية (8 مللي ثانية/إطار) على شاشات ProMotion. يُشفّر شريط إطار DevTools الألوان: أخضر = في الوقت المحدد، أصفر = تجاوز طفيف للميزانية، أحمر = تقطع شديد.

إعادة إنتاج التقطع بشكل موثوق

لا يمكنك قياس الأداء في وضع التصحيح — يُضخّم مترجم JIT الخاص بـ Dart VM والتأكيدات المُفعّلة أوقات الإطار بمقدار 3-10 أضعاف. احرص دائماً على إجراء التنصيف في وضع التنصيف (profile mode):

# تشغيل على جهاز فعلي في وضع التنصيف
flutter run --profile

# أو بناء APK/IPA للتنصيف
flutter build apk --profile
flutter build ios --profile

يُعطّل وضع التنصيف تأكيدات التصحيح ويُفعّل تجميع AOT، مما يعطي نتائج تمثيلية لما يختبره المستخدمون الفعليون. استخدم جهازاً فعلياً متوسط المستوى — لا تُعيد المحاكيات والمحاكيات الافتراضية بدقة قيود GPU.

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

// قائمة متقطعة عن قصد — لا تفعل هذا أبداً في الإنتاج
class JankyList extends StatelessWidget {
  const JankyList({super.key});

  String _heavyCompute(int index) {
    // يحاكي عملاً متزامناً ثقيلاً على خيط واجهة المستخدم
    var result = 0;
    for (var i = 0; i < 500000; i++) {
      result += i * index;
    }
    return 'Item $index: $result';
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 200,
      itemBuilder: (context, index) {
        // _heavyCompute يعمل بشكل متزامن داخل build — مصدر كلاسيكي للتقطع
        final label = _heavyCompute(index);
        return ListTile(title: Text(label));
      },
    );
  }
}
تحذير: لا تُجرِ أبداً تنصيف الأداء باستخدام flutter run (وضع التصحيح). ستكون النتائج مضللة تماماً. استخدم دائماً flutter run --profile أو flutter run --release.

فتح المخطط الزمني في Flutter DevTools

تحتوي علامة تبويب الأداء (Performance) في Flutter DevTools على طريقة عرض المخطط الزمني وأشرطة عرض الإطار — الأداتان الرئيسيتان لتشخيص التقطع.

  • ابدأ تطبيقك في وضع التنصيف: flutter run --profile
  • افتح DevTools من عنوان URL الطرفية الذي يطبعه Flutter، أو عبر flutter pub global activate devtools && flutter pub global run devtools
  • انقر على علامة تبويب الأداء واضغط تسجيل
  • أعد إنتاج التفاعل المتقطع على الجهاز
  • اضغط إيقاف وافحص التتبع المُلتقَط

قراءة أشرطة عرض الإطار

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

  • يشير الشريط الذي يتجاوز جزؤه الأزرق الخط الأحمر إلى تقطع خيط واجهة المستخدم — ابحث عن عمل مكلف في build() أو layout().
  • يشير الشريط الذي يتجاوز جزؤه الأخضر الخط إلى تقطع خيط الرسم النقطي — ابحث عن saveLayer أو التركيب الثقيل أو تجميع الشيدر للمرة الأولى.
  • انقر على أي شريط طويل لتحميل مخطط اللهب (flame chart) في المخطط الزمني أدناه. يعرض مخطط اللهب مكدس استدعاء Dart الدقيق، حتى تتمكن من رؤية الدالة التي استهلكت أكثر الوقت.
نصيحة: استخدم قائمة تحسين التتبع (Enhance Tracing) في علامة تبويب الأداء لتفعيل أحداث مخطط زمني إضافية لبناءات الودجات والتخطيطات والرسوم. هذه مُعطّلة بشكل افتراضي لأنها تضيف بعض العبء، لكنها تحدد بالضبط الودجت البطيء.

القضاء على تقطع خيط واجهة المستخدم

بمجرد تحديد دالة ساخنة في مخطط اللهب، طبّق أحد هذه الإصلاحات القياسية:

  • نقل العمل بعيداً عن خيط واجهة المستخدم — استخدم compute() أو Isolate للمهام المكثفة على المعالج (فك ترميز JSON، معالجة الصور، فرز القوائم الكبيرة).
  • تخزين النتائج المكلفة مؤقتاً — استخدم منشئات const والتذكير حتى لا يُعيد build() الحساب في كل إطار.
  • تضييق نطاق إعادة البناء — قسّم الودجات الكبيرة إلى أصغر، أو استخدم RepaintBoundary لعزل الأشجار الفرعية التي تتغير كثيراً.
  • تجنب الإدخال/الإخراج المتزامن — يجب أن تكون جميع عمليات الملف والشبكة وقاعدة البيانات غير متزامنة (Future / async-await).
import 'package:flutter/foundation.dart'; // للـ compute()

// نقل العمل الثقيل إلى عزل خلفي
Future<String> _heavyComputeIsolate(int index) {
  return compute(_isolateWork, index);
}

String _isolateWork(int index) {
  var result = 0;
  for (var i = 0; i < 500000; i++) {
    result += i * index;
  }
  return 'Item $index: $result';
}

// الإصدار المُصلَح: build() فوري الآن
class SmoothList extends StatelessWidget {
  const SmoothList({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 200,
      itemBuilder: (context, index) {
        return FutureBuilder<String>(
          future: _heavyComputeIsolate(index),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return const ListTile(title: Text('Loading...'));
            }
            return ListTile(title: Text(snapshot.data!));
          },
        );
      },
    );
  }
}

الملخص

يحدث التقطع عندما يستغرق إطار أكثر من 16 مللي ثانية للاكتمال على خيط واجهة المستخدم أو خيط الرسم النقطي. لتشخيصه: شغّل في وضع التنصيف، وافتح علامة تبويب الأداء في DevTools، وسجّل أثناء إعادة إنتاج المشكلة، وافحص أشرطة الإطار ومخطط اللهب للعثور على مكدس الاستدعاء البطيء. أصلح تقطع خيط واجهة المستخدم بإلغاء تحميل عمل المعالج إلى عزلات، وتخزين النتائج مؤقتاً، وتضييق إعادة بناء الودجات، وتجنب الاستدعاءات الحاجبة المتزامنة. يتطلب تقطع خيط الرسم النقطي (المُغطّى في درس لاحق) تجنب الاستخدام المفرط لـ saveLayer وتفعيل محرك Impeller لتقطع تجميع الشيدر.