skip to content

In Flutter, where does adding a RepaintBoundary actually help, and when does an extra one make performance worse?

level: seniorimportance: should knowfreq 44%

answer

  1. repaints climb to the nearest boundary
  2. small animator inside a static page
  3. list items already get one
  4. each boundary is another layer
  5. Highlight repaints in DevTools

basics

~20 s

A RepaintBoundary helps around a small, frequently repainting widget inside a large static area, or around a complex static subtree next to animation. Each boundary adds a layer and memory, so boundaries around things that always repaint together only add cost.

solid answer

~40 s

When a render object needs painting, Flutter repaints everything up to its nearest repaint boundary, so a spinning `CircularProgressIndicator` with no boundary nearby can repaint a whole page every frame. Wrapping the spinner, or a ticking custom painter, in a `RepaintBoundary` confines that work; wrapping a heavy static chart next to an animation keeps it out of the animation's repaints. Flutter already inserts many: each `ListView`/`GridView` child (`addRepaintBoundaries: true`), scroll viewports, route pages and the `Drawer`. Extra boundaries are not free: each is a separate layer the engine composites and may keep in memory, and a boundary around content that changes every frame anyway, or around rebuild-bound jank, gains nothing. Verify with DevTools' **Highlight repaints** (`debugRepaintRainbowEnabled`).

code

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

class ReportPage extends StatelessWidget {
  const ReportPage({super.key, required this.chart, required this.table});

  final Widget chart;
  final Widget table;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        // The spinner repaints every frame; the boundary keeps those
        // repaints from spreading to the chart and table.
        const RepaintBoundary(child: CircularProgressIndicator()),
        Expanded(child: chart),
        Expanded(child: table),
      ],
    );
  }
}

go deeper

for a junior

Recall that RepaintBoundary limits how far a repaint spreads and that lists already add one per item.

for a middle

Explain the walk up to the nearest boundary and give the two placements that help: around a small animator, or around a heavy static subtree.

for a senior

Place boundaries only from Highlight repaints evidence and profile timings, and remove ones that do not pay for their layer and memory.

for a principal

Decide where shared components should own boundaries so feature teams do not scatter them, and track the memory cost on low-end devices.

## What a repaint boundary is for Painting in Flutter happens per **render object**. When one is marked as needing paint (for example because an animation ticked), Flutter walks up to the **nearest ancestor that is a repaint boundary** and repaints that ancestor and **every descendant** in the same layer, even the ones that did not change. A **`RepaintBoundary`** widget creates a render object that always has its own layer, so it cuts that walk short in both directions: changes inside do not repaint what is outside, and changes outside do not repaint what is inside. The deeper mechanics of layers and compositing belong to the frame pipeline; for performance work the question is simply **where to put the cut**. ## Where a boundary pays off 1. **A small, constantly repainting widget in a big static screen.** A `CircularProgressIndicator` or a custom-painted clock repaints every frame and is not a boundary itself. Without one nearby, each tick can repaint the whole page area up to the next boundary. 2. **A complex, static subtree next to something that animates.** A detailed chart beside an animated counter: wrapping the chart keeps it out of the counter's repaints. The API docs add that, for sufficiently complex static subtrees, the engine may choose to rasterize and cache them. 3. **Custom scroll-like or overlay widgets you built yourself**, where Flutter does not add boundaries for you. ## Where Flutter already adds them - Each child of a `ListView` or `GridView` built through a sliver delegate, because `addRepaintBoundaries` defaults to `true`. - Scroll viewports and `SingleChildScrollView`'s render object, which are repaint boundaries themselves. - Each route's page, and the `Drawer`'s contents during its transition. - `EditableText`'s render object, so a blinking caret does not repaint the form. Adding your own boundary on top of these usually changes nothing. ## When a boundary hurts or does nothing | Situation | Effect of an extra boundary | |---|---| | Content inside and outside change on the same frames | No saving, plus one more layer to composite | | Jank comes from rebuilding widgets | None: boundaries limit painting, not `build` | | Jank comes from a saveLayer or blur | None: the offscreen work still happens inside the layer | | Hundreds of tiny boundaries | More layers, more compositing work and memory | | Complex static subtree cached by the engine | Faster frames, but cached images cost GPU memory | The UI performance guide warns that cached entries are expensive to build and take a lot of GPU memory, so boundaries should go only where the repaint pattern justifies them. ## How to place one with evidence 1. Turn on **Highlight repaints** in the DevTools Inspector (the `debugRepaintRainbowEnabled` flag). Each layer is tinted with a rotating color every time it repaints. 2. Look for a large region whose color cycles while only a small part of it actually changes. 3. Wrap the small changing part, or the large static part, in a `RepaintBoundary`. 4. Re-run: the large region should now keep one color while the small one cycles. 5. Confirm in profile mode that raster time dropped; if it did not, remove the boundary.

  • In Flutter, should you wrap every ListView item in a RepaintBoundary yourself?
    No. `ListView.builder` and the other sliver-delegate lists already wrap each item in a `RepaintBoundary` because `addRepaintBoundaries` defaults to true. Adding another inside the item duplicates the layer. The only reason to touch that flag is to set it false for very simple items where the extra layers cost more than they save.
  • Why does a RepaintBoundary not fix jank caused by a setState high in the tree?
    That jank is build work on the UI thread: `build` methods rerun and widgets are diffed. A repaint boundary only limits which render objects repaint after the build. The fix is to narrow the rebuild, for example by moving state down or using const widgets.

saying these in an interview costs you the question

  • Adding RepaintBoundary everywhere can only make an app faster.
  • A RepaintBoundary stops the widgets inside it from rebuilding.
  • ListView items need a manual RepaintBoundary to avoid repainting each other.
  • A RepaintBoundary makes a blur or Opacity inside it cheaper to rasterize.
  • Cached boundaries cost nothing beyond the first frame.