aidoc.uz

4-qism · Sifat va ishlab chiqarish·10-bob

Performance optimizatsiya va xotira boshqaruvi

Flutter ilovasini tez va silliq qilish uchun bilishingiz kerak bo'lgan barcha texnikalar — const konstruktorlardan tortib DevTools profiling va xotira sizmalarigacha, intervyu savollari bilan.

Senior Flutter dasturchidan kutiladigan asosiy narsalardan biri — nafaqat ishlaydigan kod yozish, balki nega sekinlik yuz berayotganini tushuntira olish va uni o'lchab, tuzata olish. Intervyularda ko'pincha "ilovangiz sekinlashsa, birinchi nima qilasiz?" tarzidagi savollar beriladi — javob har doim "taxmin qilish emas, profillash" bo'lishi kerak.

1. Nega performance muammolari paydo bo'ladi#

Flutter har bir freymni (frame) 16.6 millisekundda (60 FPS uchun) yoki 8.3 millisekundda (120 FPS uchun) chizishi kerak. Agar build(), layout() yoki paint() bosqichlaridan biri bu vaqtdan oshib ketsa, freym tushib qoladi — foydalanuvchi buni "jank" (sakrash, qotish) sifatida his qiladi.

Asosiy sabablar:

  • Keraksiz rebuild'lar — widget qayta qurilmasligi kerak bo'lgan holatda ham qayta quriladi.
  • Og'ir build() metodlari — build ichida qimmat hisob-kitob yoki sinxron I/O.
  • Katta ro'yxatlarni noto'g'ri render qilish — barcha elementlarni bir vaqtda qurish.
  • Xotira sizmalari (memory leaks) — dispose qilinmagan controller/subscription'lar sekin-asta xotirani band qiladi va GC (garbage collector) yukini oshiradi.

2. const konstruktorlar — eng arzon optimizatsiya#

const widget — Dart tomonidan compile vaqtida yaratiladigan, o'zgarmas obyekt. Flutter widgetni diffing qilishda, agar ikkala tomonda ham xuddi shu const instance bo'lsa, uni hatto solishtirmasdan o'tkazib yuboradi.

// Har safar rebuild bo'lganda yangi obyekt yaratiladi — chuqurroq solishtirish kerak
Widget build(BuildContext context) {
  return Padding(
    padding: EdgeInsets.all(16),
    child: Text('Salom'),
  );
}
 
// const bilan — Flutter bu subtree'ni umuman qayta ko'rib chiqmaydi
Widget build(BuildContext context) {
  return const Padding(
    padding: EdgeInsets.all(16),
    child: Text('Salom'),
  );
}

3. Rebuild'larni cheklash — "qayerda o'zgarsa, o'sha yer qursin"#

Eng katta xato — butun ekranni bitta katta StatefulWidget ichiga joylab, kichik o'zgarish uchun butun daraxtni qayta qurish. To'g'ri yondashuv — o'zgaruvchi qismni kichik, alohida widgetga ajratish.

// YOMON: har safar counter o'zgarsa, butun Column qayta quriladi
class BadCounter extends StatefulWidget {
  @override
  State<BadCounter> createState() => _BadCounterState();
}
 
class _BadCounterState extends State<BadCounter> {
  int _count = 0;
 
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const ExpensiveHeader(),   // const bo'lsa ham, parent qayta build bo'lganda chaqiriladi
        Text('$_count'),
        const ExpensiveFooter(),
      ],
    );
  }
}
 
// YAXSHI: faqat raqamni ko'rsatuvchi qism alohida widget
class GoodCounter extends StatelessWidget {
  const GoodCounter({super.key});
 
  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        ExpensiveHeader(),
        _CounterText(),
        ExpensiveFooter(),
      ],
    );
  }
}

State management kutubxonalarida bu tamoyil Selector (Provider), context.select (Riverpod), BlocSelector (Bloc) ko'rinishida ifodalanadi — ular faqat kerakli maydon o'zgarganda rebuild qiladi, butun obyekt emas. Batafsil [[07-state-management]] bobida.

ValueListenableBuilder ham xuddi shu maqsadda ishlaydi — faqat u o'rab turgan builder funksiyasini qayta chaqiradi, tashqi widget'lar tegilmaydi:

final ValueNotifier<int> counter = ValueNotifier(0);
 
ValueListenableBuilder<int>(
  valueListenable: counter,
  builder: (context, value, child) {
    return Text('$value');   // faqat shu Text qayta quriladi
  },
)

4. Katta ro'yxatlar — ListView.builder va lazy rendering#

ListView(children: [...]) barcha elementlarni darhol quradi — 1000 ta element bo'lsa, 1000 tasi ham xotiraga va render pipeline'ga tushadi. ListView.builder esa faqat ekranda ko'rinadigan (va biroz atrofidagi) elementlarni quradi:

// YOMON — 10 000 ta elementning hammasi darhol quriladi
ListView(
  children: items.map((item) => ItemTile(item: item)).toList(),
)
 
// YAXSHI — lazy, faqat ko'rinadigan qism quriladi
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ItemTile(item: items[index]),
)

Juda uzun, murakkab scroll tarkibi (masalan, header + grid + list bitta sahifada) uchun CustomScrollView + Sliver oilasi ishlatiladi — bu haqda [[06-layout-va-rendering]] bobida batafsil yozilgan.

itemCount
int
Ro'yxat uzunligi — Flutter scrollbar hajmini shundan hisoblaydi.
itemBuilder
(context, index) => Widget
Faqat kerakli index uchun chaqiriladi, natija keshlanadi va ekrandan chiqqanda tashlanadi.
cacheExtent
double
Ekrandan tashqarida oldindan qurilishi kerak bo'lgan piksel maydoni — scroll silliqligini oshiradi.

5. Rasm (image) xotira sarfi#

Rasm dekodlash — eng ko'p xotira yeydigan operatsiyalardan biri. 4000x3000 piksel JPEG fayl diskda 2 MB bo'lishi mumkin, lekin dekodlanganda xotirada ~48 MB (4000 × 3000 × 4 bayt/piksel) egallaydi — chunki ekranga chiqarish uchun u siqilmagan piksel massiviga aylantiriladi.

// YOMON — original o'lchamda dekodlanadi, 80x80 piksel joyga chiqarilsa ham
Image.network('https://example.com/katta-rasm.jpg')
 
// YAXSHI — dekodlash o'lchami cheklanadi, xotira sezilarli kamayadi
Image.network(
  'https://example.com/katta-rasm.jpg',
  cacheWidth: 160,   // fizik piksel (DPR hisobga olingan holda)
)

cached_network_image kabi kutubxonalar disk-level keshlash qo'shadi — bir xil rasm qayta yuklanmaydi, lekin cacheWidth/cacheHeight bilan dekodlash o'lchamini cheklash alohida, mustaqil optimizatsiya.

6. RepaintBoundary — qayta chizishni izolyatsiya qilish#

Flutter render daraxtini "layer"larga bo'ladi. Agar bitta widget tez-tez o'zgarsa (masalan, animatsiya), lekin uning atrofidagi statik kontent katta bo'lsa, RepaintBoundary yo'qligida butun subtree qayta chiziladi.

Column(
  children: [
    const HugeStaticImage(),   // qayta chizilishi shart emas
    RepaintBoundary(
      child: const PulsingDot(),   // faqat shu qism o'z layerida qayta chiziladi
    ),
  ],
)

DevTools'dagi "Highlight Repaints" rejimi yordamida qaysi widget'lar kutilganidan ko'proq qayta chizilayotganini vizual ko'rish mumkin.

7. Flutter DevTools — profiling qurollari#

Senior intervyuda "ilovani qanday profillaysiz" degan savolga aniq qurol nomlarini aytish kutiladi:

  1. Performance / Timeline view

    Har bir freymning qancha vaqt olganini ko'rsatadi. Qizil chiziqdan yuqori turuvchi freymlar — jank. "UI" va "Raster" thread'lari alohida ko'rsatiladi: UI thread build/layoutni, Raster thread GPU'ga chizishni bajaradi.

  2. Widget rebuild profiler

    Qaysi widget qancha marta qayta qurilganini ko'rsatadi — kutilmagan tez-tez rebuild'larni topish uchun birinchi qadam.

  3. Memory tab

    Heap hajmi vaqt bo'yicha qanday o'sishini ko'rsatadi. Doim o'suvchi, hech qachon pasaymaydigan grafik — xotira sizmasining belgisi.

  4. CPU Profiler

    Qaysi funksiya qancha CPU vaqti yeganini "flame chart" ko'rinishida ko'rsatadi — og'ir sinxron hisob-kitoblarni topish uchun.

8. Xotira sizmalari (memory leaks) — eng keng tarqalgan sabablar#

Dart'da avtomatik GC bor, lekin agar obyekt biror joyda hali ham "tinglanayotgan" bo'lsa (masalan, dispose qilinmagan StreamSubscription), GC uni tashlab yubora olmaydi — chunki texnik jihatdan u hali "ishlatilmoqda".

class _MyPageState extends State<MyPage> {
  late final AnimationController _controller;
  late final StreamSubscription _sub;
 
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _sub = someStream.listen((_) {});
  }
 
  @override
  void dispose() {
    _controller.dispose();   // yo'q bo'lsa — AnimationController "osilib qoladi"
    _sub.cancel();           // yo'q bo'lsa — Stream widget yo'qolgandan keyin ham callback chaqiraveradi
    super.dispose();
  }
}

9. Sinxron I/O va og'ir hisob — main isolate'ni band qilmaslik#

Flutter'da UI va Dart kodi bitta isolate'da (main isolate) ishlaydi. Agar shu yerda katta JSON parslash, rasm qayta ishlash yoki og'ir siklik hisob-kitob qilinsa — UI butunlay muzlaydi, chunki freym chizish uchun ham shu isolate kerak.

// YOMON — 50 000 ta elementli JSON'ni main isolate'da parslash UI'ni muzlatadi
final data = jsonDecode(hugeJsonString);
 
// YAXSHI — compute() alohida isolate'da ishlaydi, natijani Future orqali qaytaradi
final data = await compute(jsonDecode, hugeJsonString);

Bu haqda isolate mexanizmining o'zi [[03-asinxron-dasturlash]] bobida chuqurroq yoritilgan.

Ko'p uchraydigan xato javoblar#

Savol/vaziyatJunior darajadagi javobSenior darajadagi javob
"Ilova sekin, nima qilasiz?""Kodni qayta yozaman""Avval DevTools Timeline'da qaysi freym va qaysi thread (UI/Raster) sabab ekanini o'lchayman, keyin tuzataman"
ListView sekin"shrinkWrap: true qo'yaman""Avval .builder ishlatilyaptimi tekshiraman, keyin item widget'lar ichida qimmat operatsiya yo'qligini tekshiraman"
Xotira o'sib boryapti"Ilovani qayta ishga tushirish kifoya""DevTools Memory tab'da heap grafigini kuzataman, dispose() chaqirilmagan controller/subscription qidiraman"
Animatsiya qotib qoladi"Animatsiya davomiyligini kamaytiraman""Animatsiyalanuvchi qismni RepaintBoundary bilan izolyatsiya qilaman va uni chaqiruvchi build() metodini tekshiraman"

Tez-tez so'raladigan intervyu savollari#

Savol: Flutter'da freym nima va nega 16ms muhim?

Javob: 60 Hz ekranda har 16.6 ms'da bitta yangi freym chizilishi kerak. Flutter bu vaqt ichida build → layout → paint → composite zanjirini tugatishi kerak. Agar bu vaqtdan oshib ketsa, keyingi VSync signaligacha eski freym qoladi — foydalanuvchi buni "jank" sifatida ko'radi. 120 Hz ekranlarda bu byudjet 8.3 ms'gacha qisqaradi.

Savol: const widget performance'ga qanday ta'sir qiladi?

Javob: const widget compile vaqtida yaratiladi va o'zgarmas hisoblanadi. Rebuild paytida Flutter eski va yangi widget instance'ini identity bo'yicha solishtiradi (identical()) — agar ular bir xil const obyekt bo'lsa, Flutter bu subtree'ni umuman qayta ko'rib chiqmaydi, hatto == orqali chuqur solishtirishga ham hojat qolmaydi.

Savol: StatelessWidget va StatefulWidget orasidan qaysi biri "tezroq"?

Javob: To'g'ridan-to'g'ri taqqoslash noto'g'ri — ikkalasi ham xuddi shu Element/RenderObject mexanizmidan foydalanadi. Farq shundaki, StatefulWidget o'zgaruvchi holatga ega bo'lgani uchun ko'proq keraksiz rebuild manbai bo'lishi mumkin, agar holat noto'g'ri boshqarilsa. To'g'ri arxitekturada ikkalasi ham bir xil samarali.

Savol: Nega setState() butun widget subtree'sini emas, faqat shu State egasi bo'lgan widgetni qayta quradi?

Javob: setState() faqat o'sha State obyektiga tegishli Elementni "dirty" deb belgilaydi. Flutter faqat dirty deb belgilangan Element'lardan boshlab qayta quradi — lekin bu subtree pastga tushadi: agar dirty widget katta subtree'ning ildizi bo'lsa, butun subtree qayta quriladi (agar pastdagi widget'lar const bo'lmasa yoki alohida ChangeNotifier orqali izolyatsiya qilinmagan bo'lsa).

Savol: ListView.builder va oddiy ListView orasidagi farqni tushuntiring.

Javob: Oddiy ListView(children: [...]) barcha bola widget'larni darhol, ro'yxat qurilish paytida yaratadi. ListView.builder esa itemBuilder funksiyasi orqali lazy ishlaydi — faqat ekranda ko'rinadigan va cacheExtent bo'yicha yaqin atrofdagi elementlarni quradi, ekrandan chiqqan elementlarni esa tashlab yuboradi. Katta yoki cheksiz ro'yxatlar uchun .builder majburiy.

Savol: Image.network chaqirilganda xotira qanday sarflanadi va uni qanday kamaytirish mumkin?

Javob: Rasm tarmoqdan siqilgan holatda (masalan, JPEG) yuklanadi, lekin ekranga chiqarish uchun siqilmagan piksel bufferiga dekodlanadi — bu hajm original o'lchamga bog'liq, ko'rsatiladigan o'lchamga emas. cacheWidth/ cacheHeight parametrlari dekodlash o'lchamini cheklab, xotira sarfini sezilarli kamaytiradi. Qo'shimcha ravishda cached_network_image disk-level keshlash orqali qayta yuklab olishning oldini oladi.

Savol: RepaintBoundary qachon kerak, qachon kerak emas?

Javob: Kerak — kichik, tez-tez o'zgaruvchi widget katta, statik subtree ichida bo'lganda (masalan, animatsiyali indikator katta ro'yxat ustida). Kerak emas — har bir widget atrofiga qo'yish, chunki har bir RepaintBoundary o'z layerini yaratadi, bu esa GPU xotirasi va compositing xarajatini oshiradi. To'g'ri yondashuv — DevTools'da "Highlight Repaints" bilan haqiqiy muammoli joyni topib, faqat o'sha yerga qo'shish.

Savol: Flutter DevTools'da qaysi tab qaysi muammo uchun ishlatiladi?

Javob: Timeline/Performance — freym vaqtini va qaysi thread (UI/Raster) sekinlikka sabab bo'layotganini ko'rsatadi. Widget rebuild profiler — keraksiz rebuild manbalarini topadi. Memory — heap o'sishi va xotira sizmalarini kuzatadi. CPU Profiler — qaysi funksiya ko'p vaqt olayotganini flame chart orqali ko'rsatadi.

Savol: Xotira sizmasi (memory leak) Dart'da qanday yuzaga keladi, agar til GC'ga ega bo'lsa?

Javob: GC faqat "hech kim ishora qilmayotgan" obyektlarni tozalaydi. Agar StreamSubscription yoki listener dispose qilinmasa, u hali ham faol tinglab turadi — demak, u orqali bog'langan obyektlar (masalan, State) "ishlatilmoqda" deb hisoblanadi va GC tomonidan tozalanmaydi. Bu — Dart'dagi "leak"ning asosiy mexanizmi: unutilgan referenslar, C++ dagi kabi qo'lda ajratilgan xotira emas.

Savol: compute() funksiyasi nima qiladi va qachon ishlatiladi?

Javob: compute() berilgan funksiyani alohida isolate'da ishga tushiradi va natijani Future orqali qaytaradi. Og'ir, CPU-intensiv operatsiyalar (katta JSON parslash, rasm qayta ishlash, murakkab hisob-kitob) uchun ishlatiladi — bu operatsiyalar main isolate'ni band qilmasligi kerak, aks holda UI freym chizishga vaqt topa olmay muzlaydi.

Savol: Nega ba'zi jamoalar Provider/Riverpod'da Selector/ context.select ishlatishni talab qiladi, oddiy Consumer/watch emas?

Javob: Consumer/watch butun modeldagi istalgan o'zgarishda rebuild qiladi, hatto UI shu maydonga bog'liq bo'lmasa ham. Selector/context.select faqat belgilangan bitta maydon o'zgarganda rebuild qiladi — bu katta modellarda (masalan, 10+ maydonli UserProfile) keraksiz rebuild sonini sezilarli kamaytiradi.

Savol: Jank va freym tushishi (dropped frame) qanday aniqlanadi va farqi bormi?

Javob: Freym tushishi — bu jankning texnik ko'rinishi: freym 16.6 ms byudjetdan oshib ketganda, ekran eski freymni yana bir marta ko'rsatishga majbur bo'ladi (freym "tushadi"). Jank — bu foydalanuvchi tomonidan sezilaydigan natija: harakatning silliq emas, "sakrab" ko'rinishi. Ikkalasi sabab-oqibat munosabatida — freym tushishi ko'p bo'lsa, jank kuchli seziladi.

Nima uchun bu mavzu senior intervyularda ajratib so'raladi#

Junior dasturchi ko'pincha "kodni optimallashtiraman" deb umumiy javob beradi. Senior dasturchidan esa o'lchash → sabab topish → maqsadli tuzatish tsiklini aniq tushuntirish kutiladi — chunki productionda "tuyg'u bilan" optimallashtirish ko'pincha noto'g'ri joyga vaqt sarflashga olib keladi.

Amaliy topshiriq

Rebuild profilini o'qish

Haqiqiy (yoki demo) Flutter ilovasida DevTools orqali Widget rebuild profiler'ni oching. Kamida bitta ekranda kutilmagan tez-tez rebuild bo'layotgan widgetni toping va uni qanday izolyatsiya qilish (kichik widgetga ajratish, const qilish yoki Selector bilan cheklash) mumkinligini yozing.

Maslahatni ko'rsatish

DevTools'ni oching, Widget rebuild profiler yoqilgan holda ilovangizda oddiy bir ekranni bir necha marta scroll qiling yoki tugmani bosing — qaysi widget kutilganidan ko'proq marta qayta qurilganini yozib oling.