skip to content

Rebuild Scope

Const widgets, state pushed down to the smallest subtree and builder child parameters stop one setState from rebuilding a whole screen. Interviewers ask how you would find and shrink a rebuild.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Flutter, why does a const widget inside a build method skip rebuilding when its parent rebuilds, and when does it still rebuild?

level: juniorimportance: must knowfreq 70%

answer

  1. same instance every build
  2. Element.updateChild compares widgets
  3. Widget == is identity
  4. whole subtree skipped
  5. dependencies and own setState still rebuild

basics

~10 s

A const constructor call yields the same canonical instance on every build, and Element.updateChild leaves an identical child untouched. Its subtree is skipped unless an inherited dependency or its own state marks it dirty.

solid answer

~40 s

When a parent rebuilds, `Element.updateChild` compares each old child widget with the new one. If they are the **same object**, it only updates the slot and does not call `update` on the child, so the child and its whole subtree are not rebuilt. `Widget`'s `==` is identity and marked `@nonVirtual`. A `const Text('Laps')` written in `build` evaluates to one canonical instance every time, so it always hits that path. A non-const `Text('Laps')` is a new object on each call, so the element is updated and the widget's `build` runs again. A const widget still rebuilds when it is dirty for its own reasons: an `InheritedWidget` it depends on changes (a `Theme`, for example), or it is a `StatefulWidget` whose `State` calls `setState`.

code

yaml · 6 lines
yaml
include: package:flutter_lints/flutter.yaml

linter:
  rules:
    - prefer_const_constructors
    - prefer_const_literals_to_create_immutables

go deeper

for a junior

Recall that a const widget is the same instance on every build, so Flutter skips rebuilding it when the parent rebuilds.

for a middle

Explain the three paths in Element.updateChild and why identity, not equal-looking properties, decides whether a child's build runs.

for a senior

Know the limits: inherited dependencies and a child's own setState still rebuild it, and const does nothing for layout or raster cost.

for a principal

Decide which lints the team enables and how much structural refactoring const-driven rebuild savings justify on hot screens.

## What rebuild means here A **rebuild** is Flutter calling a widget's `build` again to produce a fresh description of its subtree. When a `State` calls `setState`, its element is marked dirty. On the next frame the framework calls that `State`'s `build`, then walks the returned children with **`Element.updateChild`** to reconcile them with the existing element tree. How much work a single `setState` costs depends on how far that walk goes. ## The check that makes const cheap `Element.updateChild(child, newWidget, slot)` chooses among three paths for an existing child: 1. **Same widget object** (`child.widget == newWidget`). It updates the slot if needed and returns the existing element. **No `update`, no `build`, no walk into the subtree.** 2. **Same runtime type and key** (`Widget.canUpdate`). It calls `child.update(newWidget)`, and for a stateless or stateful widget that leads to `build` running again. 3. **Otherwise** it discards the old element and inflates a new one. `Widget` declares `operator ==` as identity and marks it `@nonVirtual`, so path 1 means literally the same instance. A `const` constructor call with constant arguments is **canonicalised** by the Dart language: every evaluation yields the identical object. So `const Text('Laps')` inside `build` returns the same instance on every rebuild and always takes path 1. The non-const `Text('Laps')` allocates a new, equal-looking object each time and takes path 2. | Written in build | Instance per rebuild | Path in updateChild | Child's build runs? | |---|---|---|---| | `const Text('Laps')` | same object | 1: reuse | no | | `Text('Laps')` | new object | 2: update | yes | | `Text(label)` with a runtime `label` | new object (cannot be const) | 2: update | yes | ## When a const widget still rebuilds Const stops rebuilds that the **parent** causes. It does not freeze the subtree: - **Inherited dependencies.** If the widget, or anything inside it, called `Theme.of(context)` or `MediaQuery.sizeOf(context)`, a change to that inherited widget marks the dependent element dirty directly, whether or not its widget is const. - **Its own state.** A `const` `StatefulWidget` keeps its `State`, and that `State` can call `setState` at any time. - **Different arguments.** `const Text('a')` and `const Text('b')` are different instances, but each one is stable from build to build. ## What it buys and what it costs - **Saves build work.** Whole subtrees are skipped: no `build` calls, no widget allocations, no child diffing. On a screen where the parent rebuilds 10 times a second, such as a stopwatch, this is the cheapest win available. - **Saves allocation.** A canonical instance is created once, not on every frame. - **Needs constant arguments.** A widget built from runtime data cannot be `const`. The fix for those is structural: move the changing part into its own widget, or pass it through a builder's `child`. - **Is not a layout or paint fix.** Skipping `build` does not skip layout or paint of render objects that were marked dirty for other reasons. Raster cost is a separate problem. ## Const reaches down through a const context Inside a `const` expression, every nested constructor call and collection literal is implicitly constant. `const Column(children: [Icon(Icons.timer), Text('Laps')])` makes the `Column`, the list, the `Icon` and the `Text` constant. That is why one `const` high on a static branch often protects the whole branch. The reverse also holds. A single runtime value anywhere inside, such as a `Text(label)` or a callback closure, makes that expression non-constant, and the analyzer reports an error if you wrote `const` in front of it. The usual move is to keep the static part `const` and pull the one dynamic widget out beside it, not to drop `const` from the whole branch. ## Getting reminded to write const The Flutter docs still say the recommended `flutter_lints` set will remind you to use `const`. The pinned source disagrees. `flutter_lints` 5.0.0 removed `prefer_const_constructors`, `prefer_const_declarations` and `prefer_const_literals_to_create_immutables`, and 6.0.0, the version new projects depend on, keeps only `prefer_const_constructors_in_immutables`. A team that wants the reminder adds `prefer_const_constructors` under `linter: rules:` in `analysis_options.yaml`. ## A quick self-test Picture a page whose `State` calls `setState` on a timer. Mark every static piece of that page `const`: the `AppBar` title, the icons, the labels. The framework still calls the page's `build`, but every const child returns at path 1, so only the non-const parts are rebuilt. You can confirm this with DevTools' **Track Widget Builds**: the const children stop appearing in the timeline.

  • If Widget equality is identity, why not override operator == on your own widgets to get the same skip?
    `Widget.operator ==` is marked `@nonVirtual`, so the analyzer reports an override as a violation. The Flutter docs also warn against custom `==` on widgets, because comparing deep properties on every rebuild can cost more than the rebuild it saves. Make the instance stable instead, with `const`, a builder's `child`, or a widget cached in a `State` field.
  • Does a const widget stop its render object from being laid out or painted again?
    No. `const` only makes `updateChild` skip `update` and `build`. If a render object is marked for layout or paint for another reason, for example because an ancestor's constraints changed or a sibling needs repainting in the same layer, that work still happens. Layout and paint costs are separate diagnoses.

saying these in an interview costs you the question

  • Const widgets never rebuild, even when an inherited Theme changes
  • Flutter compares widget properties field by field to skip rebuilds
  • const only saves memory and has no effect on rebuild work
  • The default flutter_lints set warns on every missing const constructor
  • A const StatefulWidget cannot change what it shows
open as a page

A Flutter stopwatch page calls setState in its top-level State every 100 ms, rebuilding the whole Scaffold; how do you shrink each rebuild to the elapsed-time text?

level: middleimportance: must knowfreq 55%

basics

~20 s

setState rebuilds the whole subtree of the State that calls it. Move the timer and elapsed value into a small StatefulWidget that renders only the time text, or into a ValueNotifier read by one builder, and make the static rest const.

open as a page

In Flutter, what does passing a prebuilt child to a builder widget such as ValueListenableBuilder save, and when does the trick stop helping?

level: middleimportance: should knowfreq 40%

basics

~20 s

The child is built once, outside the builder callback, and handed back unchanged on every notification, so the framework skips rebuilding it. It fails when that subtree needs the value, or when the enclosing widget rebuilds and recreates it.

open as a page

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?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Run 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.

open as a page