Flutter: تحسين الأداء
الأداء هو خط أساس تجربة المستخدم — 60 إطار/ثانية وعد، 59 إطار/ثانية فشل.
📋 المتطلبات السابقة: يجب أن تكون قد أتقنت ما يلي أولًا
- الدرس 10: القوائم والتمرير
- الدرس 12: إدارة الحالة — Riverpod
1. ما ستتعلمه
- Flutter DevTools: Performance Overlay / Widget Rebuild Tracker / CPU Profiler / Memory Chart
- تحسين العرض: بُناة const / RepaintBoundary / التحميل الكسول لـ ListView.builder / تجنب إعادة بناء الويدجتات الكبيرة
- تحسين الذاكرة: استراتيجيات تخزين الصور المؤقت / AutomaticKeepAliveClientMixin / تحرير الموارد في Dispose
- تحسين الشبكة: تقسيم البيانات / CDN للصور / cached_network_image / التحميل المسبق
- ShopApp: تحسين تمرير 60 إطار/ثانية لقائمة مليون منتج
2. قصة حقيقية عن التلعثم
(1) المشكلة: تمرير قائمة المنتجات بـ 30 إطار/ثانية
قائمة منتجات ShopApp لبوب تتمر بسلاسة عند 1000 عنصر، لكنها تنخفض لـ 30 إطار/ثانية عند 10000. DevTools يكشف: كل بطاقة منتج تُطلق إعادة بناء أثناء التمرير لأن setState للويدجت الأب يسبب إعادة بناء الشجرة الفرعية بالكامل. الأسوأ أن الصور بلا تخزين مؤقت — كلما دخل عنصر نطاق العرض يُعاد تنزيله، واستخدام الذاكرة يقفز لـ 500 ميجابايت.
(2) الحل: تحسين موجّه
تحسين الأداء ليس "حسّن كل شيء" — بل إيجاد الاختناقات واستهدافها بدقة. DevTools يحدد المشكلة → RepaintBoundary يعزل → const يقلل إعادة البناء → تخزين الصور المؤقت يقلل طلبات الشبكة.
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// Product comes from the ShopApp project, full definition in Lesson 26
// Key optimizations
const ProductCard(...); // 1. const avoids rebuild
RepaintBoundary(child: Image.network(...)); // 2. Isolate repaint
CachedNetworkImage(imageUrl: ...); // 3. Cache images
AutomaticKeepAliveClientMixin; // 4. Keep tab state
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(3) النتيجة: 60 إطار/ثانية لمستوى المليون + الذاكرة انخفضت للنصف
بعد التحسين الموجّه، قائمة المليون منتج لبوب تحافظ على 60 إطار/ثانية مستقر، الذاكرة تنخفض من 500 ميجابايت إلى 200، ومعدل إصابة تخزين الصور المؤقت يصل لـ 95%.
3. نظرة عامة على تحسين الأداء
graph TD
PO[Performance Optimization] --> DEV[DevTools]
PO --> RO[Render Optimization]
PO --> MO[Memory Optimization]
PO --> NO[Network Optimization]
RO --> |const| CW[const Widgets]
RO --> |RepaintBoundary| RB2[Isolate Repaint]
MO --> |cache| IMG[Image Cache]
NO --> |pagination| PG[Pagination API]
DEV --> |Overlay| FPS[FPS Monitor]
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
4. تحليل الأداء بـ DevTools
(1) أدوات DevTools الأساسية
| الأداة | الغرض | طريقة الاستخدام |
|---|---|---|
| Performance Overlay | إطارات/ثانية فورية + وقت خيط GPU | debugShowPerformanceOverlay: true |
| Widget Rebuild Tracker | تتبع أي الويدجتات تُعاد بناؤها بشكل متكرر | DevTools → Rebuild Tracker |
| CPU Profiler | تحليل النقاط الساخنة للمعالج | DevTools → CPU Profiler |
| Memory Chart | تخصيص الذاكرة وGC | DevTools → Memory |
| Network Inspector | شلال طلبات الشبكة | DevTools → Network |
| Flutter Inspector | تصور شجرة الويدجتات | DevTools → Inspector |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تفعيل تحليل الأداء
import 'package:flutter/material.dart';
// Development mode: enable performance overlay
MaterialApp(
debugShowPerformanceOverlay: true,
showSemanticsDebugger: false,
// ...
)
// Profile mode: accurate performance measurement
// Run: flutter run --profile
// Then open DevTools: flutter pub global run devtools
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
| وضع التشغيل | التحسين | الغرض |
|---|---|---|
| Debug | بدون تحسين | التطوير والتنقيح |
| Profile | قريب من الإنتاج | تحليل الأداء |
| Release | محسّن بالكامل | إطلاق الإنتاج |
5. تحسين العرض
(1) بُناة const
| المقارنة | بدون const | مع const |
|---|---|---|
| سلوك إعادة البناء | يُنشئ نسخة جديدة كل مرة | يعيد استخدام نفس النسخة |
| canUpdate | قد لا يجتاز | يجتاز دائمًا |
| تأثير الأداء | عبء من إعادة البناء المتكرر | عبء إعادة بناء صفري |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: بطاقة منتج محسّنة بـ const
import 'package:flutter/material.dart';
// BAD: not const, rebuilds every time
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(8.0), // New instance each build
child: Text('Price: \$${product.price}'),
);
}
// GOOD: const where possible
Widget build(BuildContext context) {
return const Padding( // const padding
padding: EdgeInsets.all(8.0), // Compile-time constant
child: Text('Fixed text'), // const if content is constant
);
}
// For dynamic content, const the wrapper
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: _DynamicPrice(product: product), // Can't const dynamic
);
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: RepaintBoundary لعزل إعادة الرسم
import 'package:flutter/material.dart';
// Product comes from the ShopApp project, full definition in Lesson 26
// Without RepaintBoundary: entire list repaints when one item changes
ListView.builder(
itemCount: 1000,
itemBuilder: (_, i) => ProductCard(product: products[i]),
)
// With RepaintBoundary: only changed card repaints
ListView.builder(
itemCount: 1000,
itemBuilder: (_, i) => RepaintBoundary(
key: ValueKey(products[i].id),
child: ProductCard(product: products[i]),
),
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(2) تجنب إعادة بناء الويدجتات الكبيرة
| المشكلة | الحل |
|---|---|
| طريقة build الكبيرة تُعاد بناؤها بالكامل | قسّم إلى ويدجتات أصغر متعددة |
| setState يُعيد بناء الشجرة الفرعية بالكامل | أعد بناء الجزء المتأثر فقط |
| تغيير الويدجت الأب يؤثر على الشجرة الفرعية | استخدم مكونات فرعية const لحظر الانتشار |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تقسيم ويدجت كبيرة
import 'package:flutter/material.dart';
// BAD: entire page rebuilds on counter change
class BadPage extends StatefulWidget {
@override
State<BadPage> createState() => _BadPageState();
}
class _BadPageState extends State<BadPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
const HeavyHeader(), // Rebuilds unnecessarily!
const ProductGrid(), // Rebuilds unnecessarily!
Text('Count: $_count'), // Only this needs rebuild
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
]);
}
}
// GOOD: only counter part rebuilds
class GoodPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: [
const HeavyHeader(), // Never rebuilds (const)
const ProductGrid(), // Never rebuilds (const)
const _CounterSection(), // Only this rebuilds
]);
}
}
// HeavyHeader / ProductGrid are placeholder example Widgets
class HeavyHeader extends StatelessWidget {
const HeavyHeader({super.key});
@override Widget build(BuildContext context) => const Text('Header');
}
class ProductGrid extends StatelessWidget {
const ProductGrid({super.key});
@override Widget build(BuildContext context) => const Text('Grid');
}
class _CounterSection extends StatefulWidget {
const _CounterSection();
@override
State<_CounterSection> createState() => _CounterSectionState();
}
class _CounterSectionState extends State<_CounterSection> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
Text('Count: $_count'),
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
]);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
6. تحسين الذاكرة
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: AutomaticKeepAliveClientMixin
import 'package:flutter/material.dart';
// Product / ProductTile come from the ShopApp project, full definitions in Lessons 26/27
// Keep tab content alive when switching tabs
class ProductListTab extends StatefulWidget {
@override
State<ProductListTab> createState() => _ProductListTabState();
}
class _ProductListTabState extends State<ProductListTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; // Keep state when tab is hidden
@override
Widget build(BuildContext context) {
super.build(context); // Required for mixin
return ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(product: products[i]),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تحرير الموارد في Dispose
import 'dart:async';
import 'package:flutter/material.dart';
// DetailPage / productStream come from the ShopApp project
class _DetailPageState extends State<DetailPage> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late TextEditingController _textController;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_textController = TextEditingController();
_subscription = productStream.listen((_) => setState(() {}));
}
@override
void dispose() {
_controller.dispose(); // Release animation
_textController.dispose(); // Release text controller
_subscription?.cancel(); // Cancel stream
super.dispose();
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
7. تحسين الشبكة
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: cached_network_image لتخزين الصور المؤقت
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// product.imageUrl comes from ShopApp Product entity
CachedNetworkImage(
imageUrl: product.imageUrl,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
fadeInDuration: const Duration(milliseconds: 300),
memCacheWidth: 400, // Resize in memory to save RAM
maxWidth: 800, // Disk cache max width
)
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: واجهة API مقسمة + تحميل مسبق
import 'package:flutter/material.dart';
// Dependency: no extra dependencies (Dart/Flutter standard library sufficient)
// Product / ProductTile / LoadingIndicator come from the ShopApp project
// api.fetchProducts comes from the ShopApp network layer
class PaginatedProductList extends StatefulWidget {
@override
State<PaginatedProductList> createState() => _PaginatedProductListState();
}
class _PaginatedProductListState extends State<PaginatedProductList> {
final _controller = ScrollController();
List<Product> _products = [];
int _page = 1;
bool _hasMore = true;
@override
void initState() {
super.initState();
_loadPage();
_controller.addListener(() {
// Preload when 2 screens away from bottom
if (_controller.position.pixels >= _controller.position.maxScrollExtent * 0.8) {
_loadPage();
}
});
}
Future<void> _loadPage() async {
if (!_hasMore) return;
final newProducts = await api.fetchProducts(page: _page, limit: 20);
setState(() {
_products.addAll(newProducts);
_page++;
_hasMore = newProducts.length >= 20;
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller,
itemCount: _products.length + (_hasMore ? 1 : 0),
itemExtent: 72, // Fixed height for performance
itemBuilder: (_, i) {
if (i == _products.length) return const LoadingIndicator();
return ProductTile(product: _products[i]);
},
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
8. مثال كامل: قائمة منتجات عالية الأداء لـ ShopApp
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependencies: flutter_riverpod: ^2.0.0, cached_network_image: ^3.0.0
// Product / productProvider / cartProvider come from the ShopApp project, full definitions in Lessons 26/27
class OptimizedProductGrid extends ConsumerWidget {
const OptimizedProductGrid({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productProvider);
return productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorWidget(e),
data: (products) => GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.7,
),
itemCount: products.length,
itemBuilder: (_, i) => RepaintBoundary(
key: ValueKey(products[i].id),
child: _OptimizedProductCard(product: products[i]),
),
),
);
}
}
class _OptimizedProductCard extends ConsumerWidget {
final Product product;
const _OptimizedProductCard({required this.product});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: RepaintBoundary( // Isolate image repaint
child: CachedNetworkImage(
imageUrl: product.imageUrl,
fit: BoxFit.cover,
memCacheWidth: 200, // Save memory
placeholder: (_, __) => Container(color: Colors.grey[200]),
errorWidget: (_, __, ___) => const Icon(Icons.image_not_supported),
),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.name, maxLines: 1, overflow: TextOverflow.ellipsis),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.green)),
IconButton(
icon: const Icon(Icons.add_shopping_cart, size: 18),
onPressed: () => ref.read(cartProvider.notifier).addItem(product),
),
],
),
],
),
),
),
],
),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
❓ أسئلة شائعة
📖 ملخص
- DevTools يحدد اختناقات الأداء: FPS / Rebuild / CPU / Memory عبر أربعة أبعاد
- بُناة const تحسينات بدون تكلفة — أضفها أينما أمكن
- RepaintBoundary يعزل إعادة الرسم، يمنع التغييرات الصغيرة من إطلاق إعادة رسم واسعة
- قسّم الويدجتات الكبيرة حتى setState يؤثر فقط على الحد الأدنى من الشجرة الفرعية
- CachedNetworkImage يخزن الصور مؤقتًا؛ AutomaticKeepAlive يحافظ على حالة Tab
📝 تمارين
- أساسي (الصعوبة ⭐): استخدم Widget Rebuild Tracker في DevTools لإيجاد الويدجتات المعاد بناؤها بشكل متكرر، وحسّنها بـ const.
- متوسط (الصعوبة ⭐⭐): أضف RepaintBoundary + itemExtent لقائمة منتجات، وقارن FPS قبل وبعد التحسين.
- متقدم (الصعوبة ⭐⭐⭐): نفذ قائمة منتجات عالية الأداء كاملة: const Widget + RepaintBoundary + CachedNetworkImage(memCacheWidth) + تحميل مسبق مقسم + AutomaticKeepAliveClientMixin، محققًا 60 إطار/ثانية مستقر مع 10000 عنصر في وضع profile.