skip to content

In Flutter, why is it cheap for a crossword board of about 500 widgets to run build() again after the player types one letter?

level: middleimportance: must knowfreq 64%

answer

  1. widgets are small immutable allocations
  2. elements are reused, not recreated
  3. identical widget: walk cut off
  4. setters return early when unchanged
  5. per-child-list matching, not a tree diff

basics

~20 s

Rebuilding only allocates small immutable widget objects. The long-lived elements are reused and handed the new widgets; an identical widget stops the walk, and render object setters mark layout or paint only for values that actually changed.

solid answer

~50 s

A rebuild produces new **widget** objects, which are small immutable descriptions and cheap to allocate. The expensive parts, **elements** and **render objects**, are kept: `updateChild` gives each existing element the new widget when it has the same runtime type and key, and if the new widget is the very same instance as the old one, the child is not updated and its subtree is not walked (descendants that marked themselves dirty are still rebuilt from the dirty list). A `RenderObjectElement` then calls `updateRenderObject`, whose setters return early when a value is unchanged, so only the one changed cell's `RenderParagraph` marks itself for layout and paint. Child lists are matched in linear time per list rather than by a general tree diff. So 500 widgets cost 500 small allocations and comparisons, but only a handful of render objects do real work.

code

dart · 20 lines
dart
import 'package:flutter/material.dart';

class CrosswordCell extends StatelessWidget {
  const CrosswordCell({super.key, required this.letter, required this.filled});

  final String letter;
  final bool filled;

  @override
  Widget build(BuildContext context) {
    // New Container and Text widgets on every build. Their elements are reused;
    // the ColoredBox render object repaints only if the colour differs, and the
    // RenderParagraph re-lays out only if the text differs.
    return Container(
      color: filled ? Colors.black : Colors.white,
      alignment: Alignment.center,
      child: Text(letter),
    );
  }
}

go deeper

for a junior

Remember that widgets are cheap throwaway descriptions, and that the objects doing real work are kept between rebuilds.

for a middle

Walk through updateChild's three outcomes, the identical-instance shortcut, and how updateRenderObject setters skip unchanged values.

for a senior

Diagnose when rebuilds stop being cheap: type changes at a position, heavy build methods, and setState placed too high, and fix each at the right layer.

for a principal

Set expectations for a team: widget churn is fine by design, so review effort goes to build-time work and state placement rather than widget counts.

## The worry A crossword board is built from about **500 widgets**: a grid of cells, each a `Container` with a `Text` letter and a small clue number. The board's `State` calls `setState` when the player types, so `build` runs again and returns a fresh grid of widget objects. It sounds wasteful. It is not, for four reasons that come straight from the three-tree design. ## Reason 1: widgets are just configuration A **widget** is an `@immutable` object with a few `final` fields. It has no layout, no pixels, no listeners and no children list it manages. Allocating 500 of them is comparable to allocating 500 small records, which Dart's allocator and garbage collector handle routinely within a frame. The objects that are expensive to create are elsewhere. ## Reason 2: elements are reused Each position in the UI already has an **element** from the first build. When the board rebuilds, the element calls `updateChild(child, newWidget, slot)` for each child, and that method has three outcomes: | Situation | What `updateChild` does | |---|---| | New widget is the **identical** instance | Nothing: the child is kept and the walk stops there (descendants dirty on their own are still rebuilt) | | Same runtime type and key | Calls `child.update(newWidget)`: element, `State` and render object are kept | | Different type or key | Deactivates the old element and inflates a new one | On a crossword rebuild almost every child falls in the second row, so no element, `State` or render object is recreated. The first row matters too: a child widget passed in from above, or a `const` widget, is the same instance on each rebuild, so the framework compares a reference and skips that subtree entirely. ## Reason 3: render objects update only what changed For a **`RenderObjectElement`**, `update` calls the widget's `updateRenderObject(context, renderObject)`. Framework widgets implement it as a series of setters, and each setter compares first: - `ColoredBox` sets `color`; the render object's setter returns early if the colour is equal, otherwise calls `markNeedsPaint()`. - `Padding` sets `padding`; an equal value returns early, a new one schedules layout. - The cell whose letter changed gets a new `text` on its `RenderParagraph`, which marks it for layout and paint. So after one keystroke, one paragraph re-lays out and repaints. The other cells' render objects receive equal values and do nothing, and layout of the rest of the grid is skipped because their constraints did not change. ## Reason 4: dirty lists and linear matching - The framework keeps a **list of dirty elements** and builds only those and whatever they rebuild, not the whole app. - When a multi-child element such as a `Column` or a `Row` updates its children, it matches the old and new child lists in **linear time** per list, from both ends and then by key. Flutter's architecture notes state explicitly that it does not use a general tree-diffing algorithm. ## One keystroke, traced 1. The player types `Q` into cell 37; the board's `State` calls `setState`, which marks the board's element dirty. 2. In the next build phase the framework visits only dirty elements. The board's `build` returns a new grid of about 500 widgets. 3. For each cell, `updateChild` finds an element of the same type and key and calls `update`, so each `CrosswordCell` element rebuilds and returns new `Container` and `Text` widgets. 4. Their render object elements call `updateRenderObject`. For 499 cells every setter sees an equal value and returns. 5. Cell 37's `RenderParagraph` receives new text, compares it, and marks itself for layout. 6. Layout runs for that paragraph and whatever it affects; paint repaints only what was marked. The work that scales with 500 is allocation and comparison; the work that scales with pixels touches one cell. ## Where the cost does show up Cheap is not free. Rebuilds become a problem when: 1. a `build` method does real work, such as sorting, parsing or allocating large lists; 2. a widget of a **different type** appears at a position, forcing a new element and render object subtree; 3. `setState` is called far above the change, so thousands of widgets rebuild every frame of an animation. Measuring and reducing those costs (the `const` discipline, splitting widgets, the DevTools rebuild counts) belongs to performance tuning. The design point here is narrower: the framework is built so that recreating widget objects is the cheap operation, and everything expensive is kept and updated in place.

  • How does passing a prebuilt child widget into a parent make its rebuilds cheaper still?
    If a parent stores a child widget it received from above and returns that same instance from `build`, `updateChild` sees `child.widget == newWidget` and returns at once, without calling `update`; the walk stops there, and only descendants that are dirty on their own get rebuilt. Flutter's docs call this the reprojection pattern; `const` widgets get the same identity shortcut.
  • What makes a rebuild expensive even though widgets are cheap?
    A widget of a different runtime type at the same position forces the old element subtree to be deactivated and a new one inflated, including new render objects and new `State`. Heavy work inside `build` and `setState` called far above the change also cost time, because the framework cannot skip work you ask it to do.

saying these in an interview costs you the question

  • Every rebuild recreates the elements and render objects for the subtree.
  • Flutter diffs the whole old and new widget trees on each rebuild.
  • Rebuilding a widget always triggers layout and paint for it.
  • Widgets are expensive because each one owns a native view.
  • Only const widgets can be rebuilt without recreating render objects.