Introduction
High-refresh-rate phones (90Hz, 120Hz, and above) are increasingly common. Delivering a smooth experience on 120Hz displays requires cutting your frame work-time roughly in half: a 60Hz device gives ~16.7ms per frame, while 120Hz gives only ~8.33ms. This tutorial focuses on practical Flutter techniques to reduce repaint cost and meet the tighter frame budget. It assumes familiarity with the widget tree, RenderObjects, and basic animation concepts.
Understanding Repaints And Frame Budget
Repaints are the GPU work that occurs when a composited layer changes visual content. Flutter divides work into build, layout, compositing, and rasterization. At 120Hz, you need to minimize the expensive parts (compositing and raster) and avoid unexpected rebuilds. Use the Flutter performance overlays (Raster and CPU profiling) and the Timeline in DevTools to identify hot frames.
Key metrics: frame time distribution and the “missed frames” spike pattern. If raster time approaches 8ms, the UI will drop frames even when logic is cheap.
Practical checks:
Use the performance overlay and enable GPU profiling in DevTools.
Look for long raster sections; these indicate heavy painting.
Isolate widgets that change frequently and measure their paint cost with repaint boundaries.
Reduce Widget Rebuilds
The simplest wins come from reducing unnecessary builds. Prefer const constructors and immutable widgets. Break large build methods into smaller StatefulWidgets so that setState origin is localized. Avoid calling setState on a parent when only a child needs to change.
Use value-driven widgets that rebuild only what changes. Examples: ValueListenableBuilder or StreamBuilder (for streams). Avoid rebuilding entire lists; use ListView.builder with stable keys and item-level widgets that manage their own state.
Example: make list item self-contained to prevent parent rebuild cascading.
class ListItem extends StatelessWidget {
final String title;
const ListItem({Key? key, required this.title}) : super(key: key);
@override
Widget build(BuildContext context) => Text(title);
}Use const where possible so Flutter can skip work at build time and reuse widgets.
Use Layers And RepaintBoundary
RepaintBoundary isolates painting into a separate layer. When placed around a frequently changing widget, only that boundary is rasterized on changes instead of the entire parent subtree. But overusing them can create many layers with cost. Place RepaintBoundary where the repaint cost of the child is high and the parent rarely changes.
Example: wrap a complex animated painter with RepaintBoundary.
RepaintBoundary(
child: CustomPaint(
size: Size.infinite,
painter: MyComplexPainter(data),
),
);
class MyComplexPainter extends CustomPainter {
final Data data;
MyComplexPainter(this.data);
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(covariant MyComplexPainter old) => old.data != data;
}Use shouldRepaint judiciously: return false when the painter output does not change. For static decorations, prefer Pictures or pre-recorded layers.
Optimize Animations For 120Hz
Animations that run at the display refresh rate must do less work per frame. Use GPU-friendly widgets (Transform, Opacity with compositing) that leverage compositing layers rather than repainting pixels. When animating simple transforms or opacity, ensure the framework promotes a compositing layer (Flutter does this automatically for some cases). Avoid animating properties that force expensive paints (e.g., redrawing many shapes inside CustomPainter every frame).
Use AnimationController with vsync and prefer Tween animations that only change transform/opacity. When you must update canvas content, consider reducing update frequency: you can down-sample animation updates or run logic at half-rate and interpolate visually.
Also consider frame skipping for non-critical animations: if render time is high, animate with lower precision or use implicit animations that Flutter optimizes internally.
Vibe Studio
Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services.
Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Optimizing for 120Hz is mostly about reducing work per frame and isolating expensive painting. Measure first, then apply const and granular widgets, add RepaintBoundary where it isolates heavy paint, and prefer compositing-friendly animations. Small architectural changes—localizing state, minimizing rebuild scope, and choosing the right painting strategy—can turn a janky 120Hz experience into a fluid one.