Skip to main content

DevTools and performance

Finding out why an app is slow instead of guessing. DevTools is the primary instrument; the second half of this page is the catalogue of problems it usually reveals.

Contents

Profile mode

Debug builds are not representative — assertions run, the code is JIT-compiled, and everything is slower. Never draw a performance conclusion from a debug build.

flutter run --profile

Profile mode compiles ahead of time like release but keeps the instrumentation DevTools needs. Use a physical device; emulators have entirely different GPU characteristics.

Opening DevTools

  • From flutter run: press v, or open the URL printed in the console.
  • From VS Code: Dart: Open DevTools.
  • From Android Studio: the Open DevTools action in the run toolbar.

Flutter Inspector

Shows the widget tree as it exists on the device.

  • Select widget mode — tap something on the device and jump to it in the tree and the source.
  • Details tree — the properties of the selected widget, including the constraints it received and the size it chose. This answers most layout questions directly.
  • Layout Explorer — a visual editor for Row and Column: flex factors, alignment, and overflow, changeable live so you can find the right values before writing them.

Performance view

The frame chart is the core tool. Each bar is a frame, split into two parts:

BarThreadToo tall means
BlueUI (Dart)Build, layout, or your own code is too slow
GreenRasterPainting and GPU work is too slow

Bars above the budget line are janked frames. Click one to open the timeline events for that frame and see exactly which phase consumed the time.

Add your own markers to the timeline for code you suspect:

Timeline.timeSync('parse response', () {
_products = parseProducts(body);
});

Enhanced tracing toggles add per-widget-build, per-layout, and per-paint events. They add overhead themselves, so turn them on to find the culprit and off to measure the fix.

Rebuild tracking

Track widget builds counts rebuilds per widget per frame and shows the counts next to your source. A widget rebuilding sixty times a second when nothing about it changed is the most common and most fixable performance bug in Flutter.

Typical fixes, in order of effectiveness:

  • Add const constructors so the framework can skip the subtree entirely.
  • Extract widgets into classes instead of _build...() helper methods.
  • Narrow the subscription: context.select, Consumer, ValueListenableBuilder around the smallest possible subtree.
  • Pass the static subtree as the child argument of AnimatedBuilder, Consumer, or ListenableBuilder.

See State management and Rendering pipeline.

CPU profiler

Samples the Dart isolate and shows where time is spent, as a flame chart, call tree, or bottom-up view.

Use it when the UI thread is the problem. Record while reproducing the slow interaction, then read the bottom-up view to find the function with the most self time. Long synchronous work found here is a candidate for an isolate — see Async and isolates.

Memory view

  • Chart — heap over time, with garbage-collection events.
  • Heap snapshot — every live object by class, with retaining paths.
  • Diff two snapshots — the reliable way to find a leak.

The standard leak test: snapshot, navigate into a screen and back five times, force a garbage collection, snapshot again, diff. Any State, controller, or bloc that accumulates is leaking, almost always because something was not disposed or a subscription was not cancelled — see Lifecycle.

Images usually dominate the heap. Check ImageCache size and confirm you are decoding at display resolution rather than source resolution.

Network view

Records HTTP traffic from dart:io and package:http, with timing, headers, and bodies. Useful for two questions that look like UI problems: how many requests a screen actually issues (usually more than intended), and how much of "slow rendering" is really waiting on the network.

Debug flags

Set these temporarily in main or from the Inspector:

import 'package:flutter/rendering.dart';

void main() {
debugPaintSizeEnabled = true; // layout bounds
debugRepaintRainbowEnabled = true; // colour changes on repaint
debugPaintLayerBordersEnabled = true;
debugProfileBuildsEnabled = true;
runApp(const MyApp());
}

debugRepaintRainbowEnabled is the fastest way to find unnecessary repaints: if a large region flashes colour while only a small part of the screen is animating, add a RepaintBoundary around the animating part.

App size

flutter build apk --analyze-size --target-platform android-arm64
flutter build ipa --analyze-size

Opens a treemap of what is in the binary. Usual offenders: uncompressed images, unused fonts (especially full icon fonts), bundled models and videos, and large transitive dependencies. Also check that debug assets and test fixtures did not end up in the bundle.

Common causes of jank

SymptomCauseFix
Tall blue barsToo many rebuildsconst, extracted widgets, narrower subscriptions
Tall blue bars, one slow functionSynchronous work on the UI threadMove to an isolate, or chunk it
Tall green barsExpensive paintingRemove saveLayer triggers, add repaint boundaries
Jank while scrolling a listBuilding all childrenListView.builder, itemExtent, avoid shrinkWrap
Jank when images appearDecoding full-resolution imagescacheWidth/cacheHeight, ResizeImage, precache
Memory climbing on navigationUndisposed controllers or subscriptionsDispose in dispose, verify with a heap diff
First animation stuttersShader or pipeline warm-upVerify on Impeller; measure the first run of the route specifically
Slow start-upWork in main or the first buildDefer to after the first frame with addPostFrameCallback
Everything slow only in debugDebug modeRe-measure in profile mode

A discipline that saves time: fix one thing, re-measure, and keep the numbers. Performance work without a before-and-after is indistinguishable from superstition.

Next: Testing.