Irbisa · Cheatsheet

Performance

Rendering pipeline, jank, memory leaks, profiling tools

Senior Developer · 18 questions

  1. 01

    How do you cut unnecessary widget rebuilds (const, smaller widgets, select)?

    Make each rebuild touch as little of the tree as possible: mark unchanging subtrees const, keep state in the small widget that uses it, and subscribe to the one field you need instead of the whole object.

    // 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

    What is the difference between a rebuild and a repaint, and what does RepaintBoundary actually do?

    A rebuild re-runs build and updates widgets and elements; a repaint re-records the drawing commands of render objects.

    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

    How do you profile a Flutter app, and what do the UI and raster bars in DevTools tell you?

    Run flutter run --profile on a real, preferably cheap device, repeat one interaction and open the frame chart in DevTools Performance.

  4. 04

    A ListView with thousands of items stutters. How do you make a long list smooth?

    Build it lazily with ListView.builder (or a SliverList inside a CustomScrollView), so only the visible rows plus a small cache area exist, and keep each row cheap to build, lay out and paint.

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

    Find the bug: this order list drops frames on every fling, even though it uses 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]);        },      );}

    Every call to itemBuilder filters and sorts all 5,000 orders just to return one row, so each new row costs a full O(n log n) sort and a few rows per frame blow the 16 ms budget.

  6. 06

    This spinner makes the whole page rebuild 60 times a second. Why, and how do you scope the animation down?

    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)),      ]);}

    The listener calls setState on every tick, so the whole build (including HeavyReport) runs 60 times a second just to rotate one icon.

  7. 07

    This chart repaints on every parent rebuild even though its data never changes. What is wrong?

    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 returns true, so every rebuild that hands CustomPaint a new painter repaints the chart.

  8. 08

    Why are Opacity, BackdropFilter and anti-aliased clips expensive, and what do you use instead?

    They can force saveLayer: the subtree is drawn into an offscreen buffer and blended back, costing raster-thread time and memory in proportion to its area.

    // 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

    What is Impeller, why did it replace Skia, and what happened to shader warm-up?

    Impeller is Flutter's own rendering engine.

  10. 10

    What is tree shaking in a Flutter release build, and why can a non-constant IconData break it?

    Tree shaking is the release compiler dropping every class, function and constant the program can never reach, so unused library code never ships.

    // 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

    Pushing a details route drops frames during the first part of the transition. What is happening and how do you fix it?

    The new page's initState, first build, layout and image decoding all run during the frames that animate the transition, so any heavy work there comes straight out of the animation's budget.

  12. 12

    What causes jank in Flutter, and how do you approach fixing it in a real app?

    Jank is a frame that misses its budget (about 16 ms at 60 Hz, 8 ms at 120 Hz), so it is dropped and motion stutters.

  13. 13

    How do you find and fix memory leaks in a Flutter app?

    Almost every Flutter leak is a disposable that was never disposed, or a long-lived object (a singleton, a global stream, a static field) still holding a reference to a dead screen's State.

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

    How do you speed up cold start, and why is an app often slow only on its first launch?

    Get the first frame on screen fast and do everything else after it: keep main() free of network and heavy disk work, show a native splash, and start optional services lazily.

    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

    The app download is 90 MB and product wants it halved. How do you find the weight and cut it?

    Measure before cutting: build with --analyze-size, open the result in DevTools App Size, and attack the biggest buckets (usually assets, native SDKs pulled in by plugins, and debug symbols) while quoting the size the store actually delivers.

    # 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 shows a 40 MB Dart heap, but the OS reports 500 MB and kills the app in the background. Where is the memory?

    Most of a Flutter app's footprint is not Dart objects.

  17. 17

    Your app is smooth on your phone. How do you monitor its performance for real users in production?

    Collect frame timings and startup metrics from release builds on real devices and read percentiles per device class, either from FrameTiming callbacks you report yourself or from a tool such as Firebase Performance Monitoring or 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

    How do you stop a frame-time regression from being merged, instead of discovering it after the release?

    Script the interaction as an integration test, run it in profile mode on a fixed physical device in CI, summarise the frame timings and fail the job when a percentile gets worse than the committed baseline by more than an agreed margin.

    // 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();…