تحسين الأداء — Flutter المتقدم
تحسين الأداء في Flutter
الهدف: 60 إطاراً في الثانية (أو 120 على الشاشات الحديثة)، أي نحو 16 ملّي ثانية لبناء ورسم كل إطار. إذا تجاوزها إطار، يشعر المستخدم بـ"تقطيع" (jank).
قِس قبل أن تحسّن
- شغّل في وضع Profile (flutter run --profile) على جهاز حقيقي — وضع Debug أبطأ بكثير ولا يمثل الواقع.
- Flutter DevTools: Performance (الإطارات البطيئة وسببها: بناء أم رسم)، وCPU Profiler، وMemory، وWidget rebuild counts.
- Performance Overlay يعرض مخططي خيط الواجهة وخيط الرسم مباشرة على الشاشة.
1. قلل إعادة البناء
- const للـ Widgets الثابتة: Flutter يعيد استخدامها ولا يبنيها مجدداً.
- فصل الـ Widgets: setState يعيد بناء الـ Widget كاملاً بما تحته. انقل الجزء المتغير إلى Widget صغير خاص به، فلا يُعاد بناء الأجزاء الثقيلة (المثال الكامل يقارن الحالتين).
- مع Provider/Riverpod استمع لأصغر جزء تحتاجه (select، Consumer في أدنى مستوى) بدل مراقبة كائن كبير في أعلى الشجرة.
2. القوائم الطويلة
- ListView.builder / GridView.builder: تبني العناصر الظاهرة فقط. ListView(children: [...]) مع ألف عنصر يبنيها كلها.
- itemExtent أو prototypeItem عندما تتساوى الارتفاعات: حساب التمرير أسرع.
- تجنب القوائم المتداخلة مع shrinkWrap: true — تلغي البناء الكسول؛ استخدم CustomScrollView و Slivers.
3. الصور
- لا تعرض صورة 4000×3000 في مربع 100×100: حدد cacheWidth/cacheHeight لتُفك الصورة بحجم العرض.
- cached_network_image لتخزين صور الشبكة.
4. العمليات الثقيلة خارج خيط الواجهة
تحليل JSON ضخم أو معالجة صور في خيط الواجهة يجمّد الشاشة. انقلها إلى Isolate بـ Isolate.run(() => ...) (أو compute).
5. الرسم المكلف
- Opacity و ClipPath و الظلال الكثيرة مكلفة، خاصة داخل الحركات؛ لشفافية صورة استخدم لوناً بشفافية أو FadeTransition.
- RepaintBoundary حول جزء يتحرك كثيراً (مؤشر، رسم متحرك) يمنع إعادة رسم ما حوله.
6. حجم التطبيق وبدء التشغيل
- flutter build apk --analyze-size لمعرفة ما يضخم التطبيق.
- أجّل تحميل ما لا يلزم في الشاشة الأولى.
أخطاء شائعة
- إنشاء كائنات مكلفة (Controllers، Streams، تنسيقات) داخل build() — تُعاد مع كل بناء؛ أنشئها في initState وتخلّص منها في dispose.
- نسيان dispose لـ AnimationController و TextEditingController — تسرب ذاكرة.
- مفاتيح (Keys) مفقودة في القوائم القابلة لإعادة الترتيب.
تمرين
شغّل تطبيقك في وضع Profile وافتح DevTools، وابحث عن أكثر Widget يُعاد بناؤه. افصله أو اجعله const وقارن عدد مرات البناء قبل وبعد.
// ===== 1. استخدم const =====
// ❌ سيء
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Text('مرحباً'),
);
}
// ✅ جيد
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(16),
child: Text('مرحباً'),
);
}
// ===== 2. فصل الـ Widgets =====
// ❌ سيء - كل شيء يُعاد بناؤه
class BadExample extends StatefulWidget {
@override
_BadExampleState createState() => _BadExampleState();
}
class _BadExampleState extends State<BadExample> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Counter: $counter'),
ExpensiveWidget(), // يُعاد بناؤه كل مرة!
ElevatedButton(
onPressed: () => setState(() => counter++),
child: Text('زيادة'),
),
],
);
}
}
// ✅ جيد - فصل الـ Widgets
class GoodExample extends StatefulWidget {
@override
_GoodExampleState createState() => _GoodExampleState();
}
class _GoodExampleState extends State<GoodExample> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
CounterText(counter: counter),
const ExpensiveWidget(), // const = لا يُعاد بناؤه
IncrementButton(onPressed: () => setState(() => counter++)),
],
);
}
}
// ===== 3. ListView.builder =====
// ❌ سيء للقوائم الطويلة
ListView(
children: items.map((item) => ListTile(title: Text(item))).toList(),
)
// ✅ جيد - يبني العناصر المرئية فقط
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(title: Text(items[index])),
)
// ===== 4. RepaintBoundary =====
// لعزل الأجزاء كثيرة الرسم
RepaintBoundary(
child: AnimatedWidget(), // لن يؤثر على الـ Widgets الأخرى
)
// ===== 5. تحسين الصور =====
// استخدم cacheWidth/cacheHeight
Image.network(
'https://example.com/large-image.jpg',
cacheWidth: 300, // يُصغر في الذاكرة
cacheHeight: 300,
)
// ===== 6. AutomaticKeepAlive =====
// للحفاظ على حالة Tab
class MyTab extends StatefulWidget {
@override
_MyTabState createState() => _MyTabState();
}
class _MyTabState extends State<MyTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return ExpensiveContent();
}
}
// ===== 7. Compute للعمليات الثقيلة =====
Future<List<Item>> parseItems(String jsonString) async {
return compute(_parseItemsIsolate, jsonString);
}
List<Item> _parseItemsIsolate(String jsonString) {
final json = jsonDecode(jsonString) as List;
return json.map((e) => Item.fromJson(e)).toList();
}اختبر فهمك