In Flutter, how do you find which widgets rebuild too often on a janky screen, and what do debugProfileBuildsEnabled and DevTools' Track Widget Builds show?
answer
- profile mode first, not debug
- Track Widget Builds adds timeline events
- debugProfileBuildsEnabled, or the user-widgets variant
- event timings are inflated
- then narrow: push down, const, child, select
basics
~20 sRun in profile mode and enable Track Widget Builds in DevTools, which sets debugProfileBuildsEnabled and adds a timeline event for each widget built. Read which widgets build per frame, not the timings, then narrow the rebuild at its source.
solid answer
~40 sStart in **profile mode** so frame costs are realistic, and find the janky frames first. In the DevTools performance view, **Track Widget Builds** flips `debugProfileBuildsEnabled` through the `profileWidgetBuilds` service extension, which is registered in debug and profile builds. The timeline then carries one event per widget built, named after the widget. `debugProfileBuildsEnabledUserWidgets` limits events to widgets created in your own code, at lower overhead. The source warns that these events inflate build times, so read **which** widgets build and **how many**, not how long each takes. In debug, `debugPrintRebuildDirtyWidgets` prints `Rebuilding …` lines, and the IDE's Flutter Performance window shows rebuild counts per widget. Then fix the cause: move state down, add `const`, use a builder's `child`, or listen to a narrower slice with a select-style API.
code
dart · 8 linesimport 'package:flutter/material.dart';
void main() {
// Temporary, for one profiling session: build events for your own widgets.
// Remove before committing; the events cost time in profile builds too.
debugProfileBuildsEnabledUserWidgets = true;
runApp(const MaterialApp(home: Scaffold(body: Center(child: Text('profile me')))));
}go deeper
Know that DevTools can show which widgets build each frame, and that performance is measured in profile mode.
Explain what Track Widget Builds and debugProfileBuildsEnabled add to the timeline, and why the event timings are inflated.
Run the full loop: confirm build-phase jank, identify the rebuilding subtree and its trigger, choose the narrowest fix, and re-measure.
Decide when rebuild tuning is worth the code complexity versus the frame budget headroom a screen actually has.
## Establish that builds are the problem Rebuild tuning is worth doing only if the UI thread's **build** phase is where frame time goes. Run the app in **profile mode** (debug adds assertions and JIT overhead that distort costs), reproduce the janky interaction, and look at the frames that exceeded the budget. Reading the frame chart itself belongs to frame timing analysis. The question here starts once you suspect **too many widgets are building**. ## The tools and what each shows | Tool | Mode | What you get | |---|---|---| | DevTools performance view, **Track Widget Builds** | profile or debug | a timeline event per widget `build`, named after the widget | | `debugProfileBuildsEnabled = true` in code | profile or debug | the same events, set from `main()` instead of the UI | | `debugProfileBuildsEnabledUserWidgets = true` | profile or debug | events only for widgets created in your own code, with less overhead | | `debugPrintRebuildDirtyWidgets = true` | debug | `Building …` / `Rebuilding …` console lines per widget | | IDE **Flutter Performance** window, *Show widget rebuild information* | debug | per-widget rebuild counts for the frame and since entering the screen | The DevTools toggle and the code flag are the same switch. `WidgetsBinding` registers the `profileWidgetBuilds` and `profileUserWidgetBuilds` service extensions when not in release mode, and their setters assign `debugProfileBuildsEnabled` and `debugProfileBuildsEnabledUserWidgets`. ## Reading the result correctly - **Look at counts and shapes, not durations.** The `debugProfileBuildsEnabled` doc warns that the overhead of the timeline events is significant compared with each build, so the timings are not representative. The value is in seeing that a `Scaffold`'s whole subtree appears on every tick. - **Identical instances do not appear.** `Element.updateChild` deliberately adds no event for a child whose widget did not change, so a reused `const` subtree is absent from the timeline, which is exactly what you want to see after a fix. - **Debug adds more.** In debug builds `debugEnhanceBuildTimelineArguments` can attach widget properties to each event, which is useful for identifying which instance rebuilt but makes the trace even less representative. - **The IDE counts are a hint, not a verdict.** The IDE docs say the rebuild profiler makes you aware of rebuilds but is not by itself a diagnosis of poor performance. ## From evidence to fix Once you know which subtree rebuilds and what triggers it, pick the narrowest fix: 1. **The trigger is a `setState` high in the tree.** Move that state into a smaller widget that owns it. 2. **Static widgets rebuild with dynamic ones.** Mark them `const`, so `updateChild` reuses them. 3. **A builder rebuilds an expensive, value-independent subtree.** Pass that subtree as the builder's `child`. 4. **A widget listens to a large object but uses one field.** Listen to that field only. Core Flutter does this with aspect accessors such as `MediaQuery.sizeOf(context)` instead of `MediaQuery.of(context)`. State libraries offer `context.select`, `Selector` or `ref.select`, and each has its own API home. 5. **Every item of a long list rebuilds.** Check that items are built lazily and that per-item state lives in the item. After each change, record the same interaction again and confirm that the unwanted events are gone and the build phase shrank. ## A worked example A stopwatch screen stutters while its timer runs: 1. Record the interaction in profile mode with **Track Widget Builds** on. 2. Each 100 ms tick shows build events for `Scaffold`, `AppBar`, three buttons and every lap row. The only value that changed is the time text. 3. Trace the trigger: the page's `State` owns the timer and calls `setState`. 4. Fix: move the timer into an `ElapsedText` widget and make the static children `const`. 5. Record again. Each tick now shows `ElapsedText` and its `Text`, and the reused widgets produce no events. ## Pitfalls - Profiling in **debug** and chasing costs that do not exist in release. - Leaving `debugProfileBuildsEnabled` set in `main()`. Its timeline events cost time in profile builds too, so remove it after the investigation. - Treating every rebuild as a bug. A rebuild that produces a small subtree is cheap. The target is large subtrees rebuilt at high frequency.
- What is the difference between debugProfileBuildsEnabled and debugProfileBuildsEnabledUserWidgets?`debugProfileBuildsEnabled` adds a timeline event for every widget built, including framework internals such as the `RichText` inside a `Text`. `debugProfileBuildsEnabledUserWidgets` adds events only for widgets constructed in your own code, which cuts both noise and overhead. Each has its own service extension so tools can toggle it.
- Why can a screen with many rebuilds still hit its frame budget?A rebuild's cost depends on how much each `build` does and how large the subtree it recreates is. Many rebuilds of small, cheap widgets can fit easily in a frame. Rebuild counts point you at candidates; frame times in profile mode tell you whether they matter.
saying these in an interview costs you the question
- Profile rebuilds in debug mode, since it has more information
- The build durations shown with Track Widget Builds are accurate costs
- Track Widget Builds works in release builds too
- Every rebuild is a bug that must be eliminated
- MediaQuery.of and MediaQuery.sizeOf rebuild on exactly the same changes