Is your Flutter app suffering from jank — that frustrating stutter during animations or scrolling? This common issue often signals unnecessary UI repainting, which wastes processing power, drains battery, and degrades the user experience when Flutter struggles to maintain a smooth 60 frames per second.
Often, the fix is surprisingly simple and fast using Flutter’s RepaintBoundary widget. This widget is key to building polished, efficient applications because it lets you control exactly what gets repainted.
At Saropa, we recently identified over 15 distinct performance bottlenecks across our apps, and found that applying RepaintBoundary was usually a trivial fix — taking less than 10 minutes in most cases — that significantly reduced jank.
💥 This guide delves into why uncontrolled repainting causes jank, explains the precise mechanism of RepaintBoundary using clear analogies, and provides a practical strategy — including rules, examples, and debugging tips — for applying it effectively (and knowing when not to).
We consider this in “Jank Part 2: A Developer’s Guide to Stabilizing UI Performance”, which includes more solutions and a helpful python script to detect jank smells in your Flutter project: ➡️ found here.
Why Does Repainting Even Matter?
Every repaint consumes CPU and GPU resources. Striving for 60 FPS means Flutter has only ~16ms per frame. Unnecessary redraws cause missed deadlines, leading to jank. This wastes processing cycles, increases battery consumption, and can make devices warmer, especially as UIs grow complex.
Furthermore, as modern UIs grow more complex with animations, dynamic lists, and custom graphics, the potential for these inefficient repaints increases significantly. A small change in one area can inadvertently trigger redraws across large, visually unchanged portions of the screen without careful management.


After RepaintBoundary
Understanding RepaintBoundary
Abstract concepts are often easier to grasp with analogies. Let’s try a few to understand what RepaintBoundary does under the hood.
The Mini-Whiteboard (Isolation & Caching)
Imagine drawing static UI elements (like a background) on one large transparent sheet and frequently changing elements (like a loading spinner) on a smaller sheet placed on top. RepaintBoundary tells Flutter that the spinner is on its own sheet (layer).
This allows Flutter to only redraw that small sheet when the spinner changes and often reuse a cached version of the untouched background sheet, saving rendering work. This isolates the drawing process and enables caching of unchanged graphics.
The Toll Booth (Cost/Overhead)
Each RepaintBoundary introduces a small performance cost, like a toll booth. When Flutter’s rendering process reaches a boundary, it performs an extra check: have the contents visually changed, or can a cached image be reused?
Managing this separate graphical layer and its cache also consumes a small amount of time and memory.
This overhead is why you don’t sprinkle RepaintBoundary everywhere. Too many toll booths on short, simple roads where they aren’t needed just slow down the overall traffic (reduce performance) rather than helping it.

Rules for Using RepaintBoundary Wisely
With these analogies in mind, here are practical rules…
Rule 1: WRAP Continuous/Indefinite Animations
You should WRAP widgets like looping Lottie animations or indeterminate CircularProgressIndicator. These repaint constantly, so their high repaint cost significantly outweighs the boundary’s negligible overhead, making isolation beneficial.
Rule 2: WRAP Known Expensive Painting
It’s wise to WRAP widgets with significant per-repaint costs, such as complex CustomPaint widgets (drawing charts or intricate shapes), video players, or complex platform views. Even if they don’t repaint every frame, ensuring they only redraw when necessary and allowing Flutter to cache their output provides a substantial performance benefit.
Rule 3: WRAP Widgets Forcing Repaints (shouldRepaint: true)
Consider WRAPPING widgets that constantly force repaints, like a CustomPainter whose shouldRepaint method always returns true (perhaps mistakenly).
If a widget bypasses Flutter’s usual optimizations this way, a RepaintBoundary can act as damage control, mitigating the impact on the rest of the UI, but you should also investigate why shouldRepaint is always true.
Rule 4: AVOID Wrapping Static Content
You should AVOID wrapping widgets that rarely or never change once built, such as Text, Icon, Container with simple colors, or Padding. Since these rarely repaint, adding a boundary introduces overhead with no performance benefit.
Rule 5: AVOID Wrapping Short, Simple, Cheap Animations
Similarly, AVOID wrapping brief (< 500ms) and simple animations like fades or slides on basic widgets.
The total repaint cost during these animations is often minimal, and the RepaintBoundary’s overhead might actually be greater than the rendering cost you’d save.
Rule 6: CONSIDER Wrapping Moderately Complex Animations
This is a grey area where you should CONSIDER wrapping. Examples include animations changing multiple properties (e.g., size, position, opacity) simultaneously, those with noticeable durations (1–2 seconds), or animating gradients.
These involve more complex painting than simple fades but aren’t continuously running like loaders. Evaluate if the repaint cost seems significant enough to justify the boundary’s overhead; profiling is often the best way to decide.
Rule 7: Apply Correct Placement If Wrapping
When you do Apply a RepaintBoundary, ensure correct placement to maximize benefit and minimize cost. Place it as tightly as possible around only the dynamic or expensive widget(s) you want to isolate, excluding static parent widgets like Padding or Center from the boundary. Wrap TIGHTLY.
Show Me the Code: Examples in Action
Let’s see how these rules translate into code structure.
Example 1: Continuous Animation (Loader)
// GOOD: Boundary around the continuous animation
Column(
children: [
Text('Loading data...'), // Static
// --- RepaintBoundary NEEDED ---
RepaintBoundary( // <--- WRAP HERE
child: CircularProgressIndicator(), // Indeterminate, loops constantly
),
ElevatedButton(onPressed: () {}, child: Text('Cancel')), // Static
],
)
🚀 Example 2: Expensive Static Content (Chart)
// GOOD: Boundary around the expensive CustomPaint
Column(
children: [
Text('Sales Data'), // Updates infrequently
// --- RepaintBoundary NEEDED ---
RepaintBoundary( // <--- WRAP HERE
child: CustomPaint( // Assumes painter draws a complex, slow chart
size: Size(300, 200),
painter: ComplexChartPainter(chartData), // shouldRepaint likely compares data
),
),
Text('Last updated: ...'), // Updates infrequently
],
)
Example 3: Short/Cheap Animation (Button Fade on Tap)
// GOOD: No boundary needed for cheap, short animation
class MyButtonWithTapFade extends StatefulWidget {
final Widget child;
MyButtonWithTapFade({required this.child});
@override
_MyButtonWithTapFadeState createState() => _MyButtonWithTapFadeState();
}
class _MyButtonWithTapFadeState extends State
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _opacity;
@override
Widget build(BuildContext context) {
// --- RepaintBoundary NOT NEEDED here or around GestureDetector ---
return GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
child: FadeTransition(
opacity: _opacity,
child: widget.child, // Assumes child is relatively simple
),
);
}
}
Why: The FadeTransition runs briefly on tap. The painting cost is likely minimal. Adding a RepaintBoundary would introduce overhead probably larger than the savings.
Example 4: Correct Placement (Excluding Static Parent) — Rule 7
// GOOD Placement: Tightly around the animator
Padding( // Static framing parent - OUTSIDE the boundary
padding: EdgeInsets.all(8.0),
child: RepaintBoundary( // <--- WRAP HERE (Tightly around dynamic part)
child: AnimatedBuilder( // The part that actually changes frequently
animation: controller,
builder: (context, child) {
// Build something that moves/changes based on controller
return Transform.translate(
offset: Offset(controller.value * 100, 0),
child: Container(width: 50, height: 50, color: Colors.red),
);
},
),
),
)
// BAD Placement: Too high, includes static Padding in the layer
// RepaintBoundary( // <--- WRAPPING TOO MUCH (includes static parent)
// child: Padding(
// padding: EdgeInsets.all(8.0),
// child: AnimatedBuilder(
// animation: controller,
// builder: (context, child) {
// // ... same builder as above
// },
// ),
// ),
// )
Why: The Padding widget itself doesn’t change. Including it inside the RepaintBoundary increases the size of the layer Flutter needs to manage and potentially cache, adding unnecessary overhead. Wrap only the widget(s) that actually benefit from the boundary.
Beyond the Rules: Profiling is Key
These rules and examples provide a strong starting point for using RepaintBoundary effectively. However, they are guidelines, not absolute laws. Performance characteristics can depend on the specific widgets involved, the target device, and the complexity of the surrounding UI.
When you encounter jank or suspect a performance bottleneck, profiling your app with Flutter DevTools is the ultimate source of truth.
- Use the Performance View to see frame build times (both UI and Raster threads, often shown in the “Performance Overlay”) and identify costly frames that exceed the ~16ms budget.
- Use the CPU Profiler to dig deeper into which Dart methods are consuming the most time during frame rendering.
- Use the Widget Inspector to explore the widget tree and understand its structure.
“Users feel performance. Jank isn’t just a dropped frame; it’s a broken promise of a smooth experience. Optimize those crucial interactions.”
— Addy Osmani (Engineering Manager at Google)
But specifically for identifying repaint issues, Flutter offers two fantastic visual tools:
Visually Debugging Repaints
The easiest way to see unnecessary repaints is using the “Highlight Repaints” toggle in the Flutter Inspector tab of DevTools. Turning it on draws cycling colored borders around widgets as they repaint.
Its main advantage is interactivity: toggle it on/off easily while your app runs to see exactly what redraws when you interact with the UI. This is ideal for focused debugging.
NOTE: An older method involves setting the
debugRepaintRainbowEnabledflag in your code, but this requires code changes and a restart, making it less convenient than the DevTools toggle.
Use “Highlight Repaints” to confirm which widgets are causing jank and to verify that adding a RepaintBoundary correctly contains the repainting to within its bounds. Keep in mind this highlights painting; layout changes can still affect parents outside a boundary.

Repaint Highlights in Saropa Contacts
This is what it looks like in a real world app:



Showing repaint boundaries in Saropa Contacts
Paint Smarter, Not Harder
RepaintBoundary isn’t magic, but it’s a vital tool in your Flutter performance toolkit. By understanding why unnecessary repainting hurts performance and how RepaintBoundary helps by isolating rendering and enabling caching, you can make informed decisions about where to use it.
Using RepaintBoundary judiciously helps Flutter work smarter, not harder. It leads to smoother animations, lower resource consumption, and ultimately, a much better experience for your users.
Apply these principles, profile your app, and eliminate that jank!
“Achieving consistent 60fps in Flutter isn’t magic. It requires understanding what causes rebuilds and repaints, and strategically using tools like RepaintBoundary to isolate the hotspots.” — Filip Hráček (Flutter Developer Relations)
References
- [Video] Dive into DevTools —
- Making use of Flutter Devtools — https://goodsoft.pl/making-use-of-flutter-devtools-en/
- Question about repaints and rendering algorithms — https://forum.itsallwidgets.com/t/question-about-repaints-and-rendering-algorithms/2714
- Debugging Flutter apps programmatically — https://flutter-ko.dev/testing/code-debugging
- Flutter : How to Debug which widgets re-rendered on state change — https://stackoverflow.com/questions/50324893/flutter-how-to-debug-which-widgets-re-rendered-on-state-change
- 10 Flutter Widgets Probably Haven’t Heard Of (But Should Be Using!) — https://dcm.dev/blog/2025/01/13/ten-flutter-widgets-probably-havent-heard-of-but-should-be-using/
- How to fix performance issues in Flutter — https://dev.to/undeadlol1/how-to-fix-performance-issues-in-flutter-1h3
- Use the Flutter inspector > Highlight repaints — https://docs.flutter.dev/tools/devtools/inspector#highlight-repaints
[edit: remove excessive emoji use]
Final Word 🪅
