Isolate Your Pixels: Using RepaintBoundary for Smoother Flutter Apps
Flutter is famous for its smooth 60 FPS (and 120 FPS) performance. However, as your app grows more complex with custom painters and heavy animations, you might notice occasional frame drops or 'jank.' Often, the culprit isn't your logic—it's unnecessary repainting.
The Hidden Cost of the Paint Phase
In Flutter, the rendering process goes through several stages: Layout, Paint, and Compositing. By default, when a widget needs to repaint, it may trigger a repaint of its parent and siblings within the same layer. If you have a small moving element (like a custom loading spinner) on top of a complex background, Flutter might be re-rendering that heavy background 60 times per second, even though only the spinner changed.
What is a RepaintBoundary?
A RepaintBoundary is a widget that wraps its child and tells Flutter to isolate its painting. It creates a separate display list for that subtree. This means that if the child needs to repaint, it won't force the rest of the widget tree to repaint. Conversely, if the outside world repaints, the content inside the boundary can be reused from the cached layer.
A Practical Example
Imagine a screen with a heavy, static background and a simple animation on top. Without a boundary, the heavy background is redrawn constantly. Here is how you isolate the moving part:
import 'dart:math' as math;
import 'package:flutter/material.dart';
class PerformanceDemo extends StatefulWidget {
@override
_PerformanceDemoState createState() => _PerformanceDemoState();
}
class _PerformanceDemoState extends State<PerformanceDemo> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// A heavy static widget
const MyComplexBackground(),
// Isolate the animating widget
Center(
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: const Icon(Icons.refresh, size: 50),
);
},
),
),
),
],
),
);
}
}
How to Verify the Improvement
You shouldn't guess where to put boundaries. Flutter provides a tool called the Repaint Rainbow. You can enable it in the Flutter DevTools or by setting debugShowRepaintRainbow = true; in your main function. When enabled, widgets that repaint are highlighted with a rotating border of colors. If you see your entire screen flashing colors when only a small icon is moving, you have a performance leak that a RepaintBoundary can fix.
When to Avoid It
Don't wrap every widget in a RepaintBoundary. Creating a boundary consumes memory because Flutter has to store the pixels of that layer in the GPU. If you use it on very simple widgets, the overhead of managing the layer might actually be more expensive than just repainting the pixels. Use it specifically for expensive CustomPaint widgets or small animations sitting on top of complex trees.



