Flutter: تحسين الأداء

الأداء هو خط أساس تجربة المستخدم — 60 إطار/ثانية وعد، 59 إطار/ثانية فشل.

📋 المتطلبات السابقة: يجب أن تكون قد أتقنت ما يلي أولًا

1. ما ستتعلمه


2. قصة حقيقية عن التلعثم

(1) المشكلة: تمرير قائمة المنتجات بـ 30 إطار/ثانية

قائمة منتجات ShopApp لبوب تتمر بسلاسة عند 1000 عنصر، لكنها تنخفض لـ 30 إطار/ثانية عند 10000. DevTools يكشف: كل بطاقة منتج تُطلق إعادة بناء أثناء التمرير لأن setState للويدجت الأب يسبب إعادة بناء الشجرة الفرعية بالكامل. الأسوأ أن الصور بلا تخزين مؤقت — كلما دخل عنصر نطاق العرض يُعاد تنزيله، واستخدام الذاكرة يقفز لـ 500 ميجابايت.

(2) الحل: تحسين موجّه

تحسين الأداء ليس "حسّن كل شيء" — بل إيجاد الاختناقات واستهدافها بدقة. DevTools يحدد المشكلة → RepaintBoundary يعزل → const يقلل إعادة البناء → تخزين الصور المؤقت يقلل طلبات الشبكة.

DART
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
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(3) النتيجة: 60 إطار/ثانية لمستوى المليون + الذاكرة انخفضت للنصف

بعد التحسين الموجّه، قائمة المليون منتج لبوب تحافظ على 60 إطار/ثانية مستقر، الذاكرة تنخفض من 500 ميجابايت إلى 200، ومعدل إصابة تخزين الصور المؤقت يصل لـ 95%.


3. نظرة عامة على تحسين الأداء

100%
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]
TEXT
> الإخراج: شغّل محليًا باستخدام 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

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تفعيل تحليل الأداء

DART
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
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
وضع التشغيل التحسين الغرض
Debug بدون تحسين التطوير والتنقيح
Profile قريب من الإنتاج تحليل الأداء
Release محسّن بالكامل إطلاق الإنتاج

5. تحسين العرض

(1) بُناة const

المقارنة بدون const مع const
سلوك إعادة البناء يُنشئ نسخة جديدة كل مرة يعيد استخدام نفس النسخة
canUpdate قد لا يجتاز يجتاز دائمًا
تأثير الأداء عبء من إعادة البناء المتكرر عبء إعادة بناء صفري

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: بطاقة منتج محسّنة بـ const

DART
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
  );
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: RepaintBoundary لعزل إعادة الرسم

DART
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]),
  ),
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(2) تجنب إعادة بناء الويدجتات الكبيرة

المشكلة الحل
طريقة build الكبيرة تُعاد بناؤها بالكامل قسّم إلى ويدجتات أصغر متعددة
setState يُعيد بناء الشجرة الفرعية بالكامل أعد بناء الجزء المتأثر فقط
تغيير الويدجت الأب يؤثر على الشجرة الفرعية استخدم مكونات فرعية const لحظر الانتشار

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تقسيم ويدجت كبيرة

DART
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('+')),
    ]);
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. تحسين الذاكرة

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: AutomaticKeepAliveClientMixin

DART
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]),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تحرير الموارد في Dispose

DART
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();
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

7. تحسين الشبكة

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: cached_network_image لتخزين الصور المؤقت

DART
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
)
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: واجهة API مقسمة + تحميل مسبق

DART
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]);
      },
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

8. مثال كامل: قائمة منتجات عالية الأداء لـ ShopApp

DART
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),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س هل أستخدم وضع debug أم profile لتحليل الأداء؟
ج يجب استخدام وضع profile. وضع debug يتضمن الكثير من تأكيدات الفحص التي تجعل قراءات FPS غير دقيقة.
س هل أضيف const أينما أمكن؟
ج نعم. بُناة const هي تحسينات بدون تكلفة — يمكنك إضافتها كلما كانت جميع معاملات البناء ثوابت وقت الترجمة.
س هل هناك ضرر من إضافة كثير من RepaintBoundaries؟
ج نعم. كل RepaintBoundary يُنشئ طبقة رسم مستقلة، مما يزيد استخدام الذاكرة. أضفها فقط عند نقاط العزل حيث إعادة الرسم متكررة.
س لماذا يحسّن itemExtent الأداء؟
ج بارتفاعات عناصر ثابتة، لا يحتاج ListView لقياس كل عنصر فرعي — يمكنه حساب الإزاحات مباشرة، مما يجعل التمرير أكثر سلاسة.
س ما الفرق بين CachedNetworkImage وImage.network؟
ج Image.network بلا تخزين مؤقت ويُعيد التنزيل كل مرة؛ CachedNetworkImage يوفر تلقائيًا تخزينًا مؤقتًا على القرص + الذاكرة، ويدعم ويدجتات العنصر النائب والخطأ.
س كيف أكشف تسرب الذاكرة؟
ج استخدم لوحة Memory في DevTools لعرض اتجاهات الذاكرة. إذا استمرت الذاكرة بالنمو ولم تنخفض أبدًا، فمن المرجح وجود تسرب. أسباب شائعة: متحكمات لم تُحرر، Streams لم تُلغَ.

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): استخدم Widget Rebuild Tracker في DevTools لإيجاد الويدجتات المعاد بناؤها بشكل متكرر، وحسّنها بـ const.
  2. متوسط (الصعوبة ⭐⭐): أضف RepaintBoundary + itemExtent لقائمة منتجات، وقارن FPS قبل وبعد التحسين.
  3. متقدم (الصعوبة ⭐⭐⭐): نفذ قائمة منتجات عالية الأداء كاملة: const Widget + RepaintBoundary + CachedNetworkImage(memCacheWidth) + تحميل مسبق مقسم + AutomaticKeepAliveClientMixin، محققًا 60 إطار/ثانية مستقر مع 10000 عنصر في وضع profile.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%