Irbisa · Шпаргалка

Производительность

Конвейер рендеринга, jank, утечки памяти, инструменты профилирования

Senior-разработчик · 18 вопросов

  1. 01

    Как сократить лишние ребилды виджетов (const, мелкие виджеты, select)?

    Каждый ребилд должен затрагивать как можно меньшую часть дерева: помечайте неизменные поддеревья const, держите состояние в том маленьком виджете, который его использует, и подписывайтесь на одно нужное поле, а не на весь объект.

    // before: one counter rebuilds the whole page on every tapclass _CatalogPageState extends State<CatalogPage> {  int _count = 0;   @override  Widget build(BuildContext context) => Column(children: […
  2. 02

    Чем ребилд отличается от перерисовки (repaint) и что на самом деле делает RepaintBoundary?

    Ребилд заново вызывает build и обновляет виджеты и элементы; перерисовка заново записывает команды рисования рендер-объектов.

    import 'package:flutter/rendering.dart'; void main() {  // Debug only: a layer's border changes colour every time it repaints  debugRepaintRainbowEnabled = true;  runApp(const MyApp());…
  3. 03

    Как профилировать Flutter-приложение и что показывают столбики UI и raster в DevTools?

    Запустите flutter run --profile на реальном, желательно дешёвом устройстве, повторяйте одно и то же действие и откройте график кадров во вкладке Performance в DevTools.

  4. 04

    ListView на тысячи элементов тормозит при скролле. Как сделать длинный список плавным?

    Стройте его лениво через ListView.builder (или SliverList внутри CustomScrollView), чтобы существовали только видимые строки и небольшая область кэша, и делайте каждую строку дешёвой в построении, раскладке и отрисовке.

    class FeedList extends StatelessWidget {  const FeedList({super.key, required this.posts});  final List<Post> posts;   @override  Widget build(BuildContext context) {…
  5. 05

    Найдите баг: этот список заказов теряет кадры при каждом флинге, хотя построен на ListView.builder.

    class OrdersList extends StatelessWidget {  const OrdersList({super.key, required this.orders, required this.query});  final List<Order> orders; // about 5,000 orders  final String query;   @override  Widget build(BuildContext context) => ListView.builder(        itemCount: orders.where((o) => o.title.contains(query)).length,        itemBuilder: (context, i) {          final visible = orders.where((o) => o.title.contains(query)).toList()            ..sort((a, b) => b.date.compareTo(a.date));          return OrderTile(order: visible[i]);        },      );}

    Каждый вызов itemBuilder фильтрует и сортирует все 5 000 заказов ради одной строки, поэтому каждая новая строка стоит полной сортировки за O(n log n), и несколько строк за кадр выходят за бюджет в 16 мс.

  6. 06

    Этот спиннер заставляет всю страницу перестраиваться 60 раз в секунду. Почему и как сузить анимацию?

    class _SyncPageState extends State<SyncPage>    with SingleTickerProviderStateMixin {  late final _ctrl = AnimationController(      vsync: this, duration: const Duration(seconds: 1))    ..addListener(() => setState(() {}))    ..repeat();   @override  Widget build(BuildContext context) => Column(children: [        Transform.rotate(          angle: _ctrl.value * 2 * pi,          child: const Icon(Icons.sync),        ),        Expanded(child: HeavyReport(rows: widget.rows)),      ]);}

    Слушатель вызывает setState на каждом тике, поэтому весь build (вместе с HeavyReport) выполняется 60 раз в секунду только ради поворота одной иконки.

  7. 07

    Этот график перерисовывается при каждом ребилде родителя, хотя его данные не меняются. Что не так?

    class ChartPainter extends CustomPainter {  ChartPainter(this.points);  final List<double> points;   @override  void paint(Canvas canvas, Size size) {    // draws a polyline through points  }   @override  bool shouldRepaint(ChartPainter oldDelegate) => true;} // In the parent's build():CustomPaint(painter: ChartPainter(state.points.toList()))

    shouldRepaint возвращает true, поэтому каждый ребилд, который передаёт CustomPaint новый painter, перерисовывает график.

  8. 08

    Почему Opacity, BackdropFilter и клипы со сглаживанием дорогие и чем их заменить?

    Они могут вызывать saveLayer: поддерево рисуется в offscreen-буфер и смешивается обратно, что стоит времени raster-потока и памяти пропорционально площади.

    // before: an offscreen layer just to dim one labelOpacity(opacity: 0.5, child: Text('Archived')) // after: the alpha is part of the paint, no extra layerText(  'Archived',…
  9. 09

    Что такое Impeller, почему он заменил Skia и что стало с прогревом шейдеров?

    Impeller представляет собой движок рендеринга Flutter.

  10. 10

    Что такое tree shaking в release-сборке Flutter и почему его может сломать неконстантный IconData?

    Tree shaking означает, что компилятор release-сборки выбрасывает все классы, функции и константы, до которых программа никогда не дойдёт, поэтому неиспользуемый код библиотек не попадает в приложение.

    // wrong: the glyph is chosen at runtime, so the font cannot be tree shakenIcon(IconData(category.codePoint, fontFamily: 'MaterialIcons')) // right: every glyph is a compile-time constantconst categoryIcons = <String, IconData>{  'food': Icons.restaurant,…
  11. 11

    При переходе на экран деталей теряются кадры в первой части анимации. Что происходит и как это исправить?

    initState новой страницы, её первый build, раскладка и декодирование изображений выполняются в тех же кадрах, что анимируют переход, поэтому любая тяжёлая работа там вычитается прямо из бюджета анимации.

  12. 12

    Что вызывает jank во Flutter и как подходить к его устранению в реальном приложении?

    Под jank понимают кадр, не уложившийся в бюджет (около 16 мс при 60 Гц, 8 мс при 120 Гц), поэтому он отбрасывается и движение дёргается.

  13. 13

    Как найти и устранить утечки памяти во Flutter-приложении?

    Почти за любой утечкой во Flutter стоит disposable-объект, который забыли освободить, или долгоживущий объект (синглтон, глобальный стрим, статическое поле), всё ещё держащий ссылку на State закрытого экрана.

    class _ChatState extends State<ChatScreen> {  final _input = TextEditingController();  final _scroll = ScrollController();  StreamSubscription<Message>? _sub;  Timer? _typing;…
  14. 14

    Как ускорить холодный старт и почему приложение часто медленное только при первом запуске?

    Выведите первый кадр как можно быстрее, а всё остальное делайте после него: в main() не должно быть сети и тяжёлой работы с диском, покажите нативный сплэш, необязательные сервисы запускайте лениво.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  // Only what the first screen cannot render without  final prefs = await SharedPreferences.getInstance();  runApp(App(prefs: prefs));…
  15. 15

    Приложение весит 90 МБ, продукт хочет вдвое меньше. Как найти, что занимает место, и урезать?

    Сначала измерьте: соберите с --analyze-size, откройте результат в DevTools App Size и атакуйте самые крупные статьи (обычно это ассеты, нативные SDK из плагинов и отладочные символы), а называйте тот размер, который реально отдаёт стор.

    # Size report per package, library and asset (open the JSON in DevTools > App Size)flutter build appbundle --release --analyze-size --target-platform android-arm64flutter build ipa --release --analyze-size # Smaller binary: symbols move out; keep them for crash symbolicationflutter build appbundle --release \…
  16. 16

    DevTools показывает Dart-кучу на 40 МБ, а ОС показывает 500 МБ и убивает приложение в фоне. Где память?

    Большая часть памяти Flutter-приложения приходится не на Dart-объекты.

  17. 17

    На вашем телефоне приложение плавное. Как мониторить его производительность у реальных пользователей?

    Собирайте тайминги кадров и метрики старта с release-сборок на реальных устройствах и смотрите перцентили по классам устройств: либо через колбэки FrameTiming, которые отправляете сами, либо через инструмент вроде Firebase Performance Monitoring или Sentry.

    void startFrameMonitoring(void Function(Map<String, double>) report) {  final buildMs = <double>[];  final rasterMs = <double>[];   SchedulerBinding.instance.addTimingsCallback((List<FrameTiming> timings) {    for (final t in timings) {…
  18. 18

    Как не дать регрессии времени кадра попасть в main, а не узнавать о ней после релиза?

    Опишите сценарий как интеграционный тест, запускайте его в profile-режиме на фиксированном физическом устройстве в CI, суммируйте тайминги кадров и роняйте джобу, когда перцентиль хуже закоммиченного baseline больше чем на оговорённый порог.

    // integration_test/scroll_perf_test.dartvoid main() {  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();  testWidgets('feed scrolls within budget', (tester) async {    await tester.pumpWidget(const App());    await tester.pumpAndSettle();…