Flutter Mobile Development · Lección

RepaintBoundary, widgets Const y poda de reconstrucciones

Reduzca los repintados y las reconstrucciones innecesarios mediante límites y constructores const.

Lección 3 de 413 pasos

RepaintBoundary, widgets Const y poda de reconstrucciones es una lección gratuita de Flutter Mobile Development en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Flutter Mobile Development, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Flutter Mobile Development incluye 4 lecciones en total.

Partes de esta lección aún no han sido traducidas y se muestran en inglés.

Rebuilds vs Repaints

Flutter performance work lives at three layers, and conflating them wastes effort:

  • Rebuild — build() runs again, producing a new widget tree. Cheap if widgets are immutable and shallow, but it can cascade.
  • Relayout — RenderObjects recompute size/position. Triggered by constraint or child changes.
  • Repaint — pixels are re-rasterized to a layer. Expensive for gradients, shadows, and complex paths.

This lesson targets two distinct wins: pruning rebuilds with const and structure, and isolating repaints with RepaintBoundary. They solve different problems — do not reach for one when you need the other.

Why const Widgets Skip Rebuilds

A const widget is canonicalized by the Dart compiler: every evaluation of the same const expression returns the identical instance. When a parent rebuilds, Flutter compares the new child widget to the old one. If they are the same instance (identical(old, new) is true), Flutter short-circuits and does not rebuild that subtree at all.

  • Without const, each parent build creates a fresh Widget object, forcing the element to update its child.
  • With const, the canonical instance is reused, so the subtree is skipped entirely.

This is the cheapest optimization in Flutter — it costs you a keyword and prunes whole branches of the rebuild.

// Dart compile-time canonicalization: const instances are identical.
class Point {
  final int x;
  final int y;
  const Point(this.x, this.y);
}

void main() {
  const a = Point(1, 2);
  const b = Point(1, 2);
  // Both refer to the SAME canonical instance.
  print(identical(a, b)); // true

  final c = Point(1, 2); // runtime instance, not canonicalized
  print(identical(a, c)); // false
}

Applying const in a Widget Tree

Mark every widget you can as const. The rule: a widget can be const if all its constructor arguments are themselves compile-time constants. Static labels, icons, padding, and spacers are prime candidates.

  • A const child inside a frequently-rebuilding parent is the highest-value placement — the parent rebuilds, the child is skipped.
  • Enable the prefer_const_constructors and prefer_const_literals_to_create_immutables lints so the analyzer flags missed opportunities.

Below, only the live counter text changes; the title, divider, and icon never rebuild because they are const.

class CounterPanel extends StatelessWidget {
  final int count;
  const CounterPanel({super.key, required this.count});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('Live Counter'),      // const: skipped on rebuild
        const Divider(),                  // const: skipped
        Text('$count'),                   // dynamic: must rebuild
        const Icon(Icons.timer),          // const: skipped
      ],
    );
  }
}

When const Is Impossible: Hoist the Subtree

You cannot make a widget const if it depends on runtime values (a theme color, a fetched string, a callback closure). But you can still avoid rebuilding it by hoisting it out of the rebuilding scope.

  • Build the expensive-but-static subtree once in a parent, store it in a field or pass it as a child parameter, and let the rebuilding widget reuse that same reference.
  • This is exactly the pattern AnimatedBuilder and ValueListenableBuilder use with their child argument: the child is built once and threaded through every animation frame untouched.
// The child is built ONCE and reused on every animation tick.
AnimatedBuilder(
  animation: rotationController,
  // 'child' is created a single time, not per-frame.
  child: const ExpensiveLogo(),
  builder: (context, child) {
    return Transform.rotate(
      angle: rotationController.value * 6.28,
      child: child, // reused reference: no rebuild of ExpensiveLogo
    );
  },
)

How Repaints Propagate

Flutter composites the UI into layers. When a RenderObject is marked dirty for paint, Flutter repaints the entire layer it belongs to — not just that one object. By default, sibling widgets often share a layer.

  • An animation that constantly repaints (a spinner, a progress bar, a blinking cursor) marks its layer dirty every frame.
  • If a heavy static widget (a big image, a complex gradient background) shares that layer, it gets re-rasterized every frame too — pure waste.

The fix is to give the volatile widget its own layer so its repaints stay contained. That is what RepaintBoundary does.

RepaintBoundary: Isolating a Layer

RepaintBoundary wraps a subtree and forces it into a separate compositing layer. Repaints inside the boundary no longer dirty the parent layer, and repaints outside no longer force the boundary's contents to re-rasterize.

  • Wrap the frequently repainting widget so its churn stays isolated, OR
  • Wrap the expensive static widget so a noisy neighbor can't drag it into per-frame repaints.

The boundary has a real cost: an extra layer means extra memory and a compositing step. Use it surgically where the profiler shows repaint waste — not everywhere.

Stack(
  children: [
    // Heavy, static background — isolate it so the spinner can't
    // force it to re-rasterize every frame.
    const RepaintBoundary(
      child: ComplexGradientBackground(),
    ),
    // Constantly repainting overlay gets its own layer too.
    const Center(
      child: RepaintBoundary(
        child: CircularProgressIndicator(),
      ),
    ),
  ],
)

Lists Insert RepaintBoundary for You

A common question: do you need to wrap every ListView item in a RepaintBoundary? Usually no — ListView, GridView, and other SliverList-based widgets already wrap each item in a RepaintBoundary by default (controlled by the addRepaintBoundaries flag, which defaults to true).

  • This means scrolling repaints don't cascade across all visible rows.
  • Wrapping items again is redundant and adds layer overhead. Leave the default on.
  • You set addRepaintBoundaries: false only for trivially cheap items where the extra layer costs more than it saves.
ListView.builder(
  itemCount: messages.length,
  // addRepaintBoundaries defaults to true — each row is already isolated.
  itemBuilder: (context, index) {
    return MessageTile(message: messages[index]);
  },
)

setState Rebuilds the Whole build()

Calling setState marks the entire State's build() method dirty. Everything that method returns is rebuilt — even widgets unrelated to the changed value. In a large screen this is the most common source of jank.

  • The narrower your build(), the cheaper each setState.
  • Push the volatile state down into the smallest possible widget so only it rebuilds.

Below, a single tap rebuilds the whole screen — including the static header and footer — because they live in the same build() as the counter.

class DashboardState extends State<Dashboard> {
  int taps = 0;

  @override
  Widget build(BuildContext context) {
    // setState rebuilds ALL of this, header and footer included.
    return Column(
      children: [
        const ExpensiveHeader(),
        Text('Taps: $taps'),
        ElevatedButton(
          onPressed: () => setState(() => taps++),
          child: const Text('Tap'),
        ),
        const ExpensiveFooter(),
      ],
    );
  }
}

Pruning with ValueListenableBuilder

To rebuild only the part that depends on a value, replace setState with a scoped listenable. ValueListenableBuilder rebuilds just its builder closure when the value changes; everything outside it stays put.

  • The header, footer, and surrounding layout are built once and never rebuild on value changes.
  • Combine with the child argument to hoist any static subtree inside the builder.

This converts a whole-screen rebuild into a surgical, single-widget rebuild.

final taps = ValueNotifier<int>(0);

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      const ExpensiveHeader(), // built once, never rebuilt
      ValueListenableBuilder<int>(
        valueListenable: taps,
        builder: (context, value, child) => Text('Taps: $value'),
      ),
      ElevatedButton(
        onPressed: () => taps.value++, // no setState
        child: const Text('Tap'),
      ),
      const ExpensiveFooter(), // built once, never rebuilt
    ],
  );
}

Measuring Before You Optimize

Never guess — measure. Flutter ships tools that show exactly where rebuilds and repaints happen:

  • Repaint Rainbow (DevTools / debugRepaintRainbowEnabled): overlays a rotating border color on each layer every time it repaints. A widget that flickers colors constantly is repainting too often — a candidate for RepaintBoundary.
  • Track Widget Builds / Rebuild Stats: counts how many times each widget rebuilds, exposing widgets that rebuild far more than expected.
  • Performance Overlay: shows the UI (build) and raster (paint) thread budgets; spikes tell you which thread is the bottleneck.

Optimize only what the profiler flags. A const or a boundary added blindly can cost more than it saves.

import 'package:flutter/rendering.dart';

void main() {
  // Visualize layer repaints during development.
  debugRepaintRainbowEnabled = true;
  runApp(const MyApp());
}

A Decision Checklist

Put the three tools in order when you face jank:

  • Too many rebuilds? Add const where arguments are constant; hoist static subtrees via the child parameter; scope state with ValueListenableBuilder instead of screen-wide setState.
  • Too many repaints? Wrap the volatile or the expensive widget in RepaintBoundary to isolate its layer.
  • Not sure which? Turn on Rebuild Stats and Repaint Rainbow first.

Key distinction: const fights rebuilds; RepaintBoundary fights repaints. Using the wrong one leaves the bottleneck untouched and may add overhead.

Quick Check

A screen shows a static, expensive blurred background image. On top of it sits a small CircularProgressIndicator that animates every frame. The profiler shows the whole background re-rasterizing 60 times per second. What is the correct fix?

Recap

You now have a precise mental model for cutting Flutter render cost:

  • Rebuilds are pruned by const (canonical instances are skipped via identical), by hoisting static subtrees through the child parameter, and by scoping state with ValueListenableBuilder instead of whole-screen setState.
  • Repaints are isolated by RepaintBoundary, which moves a subtree onto its own compositing layer so volatile and static widgets stop contaminating each other.
  • Lists already insert repaint boundaries per item — don't double-wrap.
  • Measure first with Rebuild Stats, Repaint Rainbow, and the Performance Overlay; apply each tool only where the profiler proves it pays off.

The golden rule: const fights rebuilds, RepaintBoundary fights repaints — match the tool to the symptom.

Gratis para empezar

Aprende Dart con un tutor de IA — gratis

Escribe y ejecuta código real en tu navegador, obtén ayuda instantánea de un tutor de IA disponible 24/7 y continúa donde lo dejaste en la web o en la aplicación.

Cursos
22
Lecciones
88

Preguntas frecuentes

¿La lección «RepaintBoundary, widgets Const y poda de reconstrucciones» es gratis?

Sí — el texto completo de «RepaintBoundary, widgets Const y poda de reconstrucciones» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Flutter Mobile Development, actualiza a CoddyKit PRO. El curso de Flutter Mobile Development incluye 4 lecciones en total.

¿Qué aprenderé en «RepaintBoundary, widgets Const y poda de reconstrucciones»?

Reduzca los repintados y las reconstrucciones innecesarios mediante límites y constructores const. Practicas Flutter Mobile Development con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar Flutter Mobile Development?

No se requiere experiencia previa. Flutter Mobile Development en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.

¿Cuánto tiempo toma la lección «RepaintBoundary, widgets Const y poda de reconstrucciones»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de Flutter Mobile Development?

Sí. Cada lección de Flutter Mobile Development incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Los tres árboles: Widget, Element y RenderObject
  2. Análisis de jank con la línea de tiempo de DevTools
  3. RepaintBoundary, widgets Const y poda de reconstrucciones
  4. Calentamiento de shaders y migración a Impeller
← Volver a Flutter Mobile Development