RepaintBoundary, widgety const i ograniczanie przebudowy
Ograniczaj niepotrzebne przemalowywanie i przebudowy za pomocą granic oraz konstruktorów const
RepaintBoundary, widgety const i ograniczanie przebudowy to bezpłatna lekcja Flutter Mobile Development na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Flutter Mobile Development, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Flutter Mobile Development zawiera 4 lekcji w sumie.
Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.
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 freshWidgetobject, 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
constchild inside a frequently-rebuilding parent is the highest-value placement — the parent rebuilds, the child is skipped. - Enable the
prefer_const_constructorsandprefer_const_literals_to_create_immutableslints 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
childparameter, and let the rebuilding widget reuse that same reference. - This is exactly the pattern
AnimatedBuilderandValueListenableBuilderuse with theirchildargument: 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: falseonly 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 eachsetState. - 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
childargument 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 forRepaintBoundary. - 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
constwhere arguments are constant; hoist static subtrees via thechildparameter; scope state withValueListenableBuilderinstead of screen-widesetState. - Too many repaints? Wrap the volatile or the expensive widget in
RepaintBoundaryto 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 viaidentical), by hoisting static subtrees through thechildparameter, and by scoping state withValueListenableBuilderinstead of whole-screensetState. - 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.
Często zadawane pytania
Czy lekcja „RepaintBoundary, widgety const i ograniczanie przebudowy” jest bezpłatna?
Tak — pełny tekst „RepaintBoundary, widgety const i ograniczanie przebudowy” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Flutter Mobile Development, przejdź na CoddyKit PRO. Kurs Flutter Mobile Development zawiera 4 lekcji w sumie.
Co nauczysz się w „RepaintBoundary, widgety const i ograniczanie przebudowy”?
Ograniczaj niepotrzebne przemalowywanie i przebudowy za pomocą granic oraz konstruktorów const Ćwiczysz Flutter Mobile Development z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Flutter Mobile Development?
Nie wymagamy żadnego doświadczenia. Flutter Mobile Development w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „RepaintBoundary, widgety const i ograniczanie przebudowy”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Flutter Mobile Development?
Tak. Każda lekcja Flutter Mobile Development zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Trzy drzewa: Widget, Element i RenderObject
- Profilowanie zacięć za pomocą osi czasu DevTools
- RepaintBoundary, widgety const i ograniczanie przebudowy
- Rozgrzewanie shaderów i migracja do Impellera