Distinguish Build/UI Work from Raster Work in a Janky Screen
Use Flutter DevTools performance profiler to diagnose frame drops, isolate CPU build bottlenecks from GPU raster shaders/clips, and apply RepaintBoundary fixes.
1. The Core Challenge & Problem
2. Architectural Principles & Resolution
3. Complete Tested Flutter Code
main.dart (Flutter 3.x+ ready)import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: RepaintBoundaryPerformanceDemo()));
class RepaintBoundaryPerformanceDemo extends StatefulWidget {
const RepaintBoundaryPerformanceDemo({super.key});
@override
State<RepaintBoundaryPerformanceDemo> createState() => _RepaintBoundaryPerformanceDemoState();
}
class _RepaintBoundaryPerformanceDemoState extends State<RepaintBoundaryPerformanceDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('T15.01: UI vs Raster Profiling')),
body: Stack(
children: [
// 1. Static heavy background with 100 decorated cards
// Without RepaintBoundary, this entire background repaints on EVERY animation frame!
const HeavyStaticBackground(),
// 2. High-frequency active animation
// CRITICAL OPTIMIZATION: Wrap in RepaintBoundary to isolate raster repaint dirty area!
Center(
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: child,
);
},
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: const Color(0xFFEF4444),
borderRadius: BorderRadius.circular(24),
boxShadow: const [
BoxShadow(color: Color(0x66EF4444), blurRadius: 20, offset: Offset(0, 8)),
],
),
child: const Center(
child: Icon(Icons.bolt, color: Colors.white, size: 48),
),
),
),
),
),
],
),
);
}
}
class HeavyStaticBackground extends StatelessWidget {
const HeavyStaticBackground({super.key});
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 5),
itemCount: 50,
itemBuilder: (context, i) {
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Center(child: Text('#$i', style: TextStyle(color: Colors.grey[500]))),
);
},
);
}
} 4. Expected Visual & Behavioral Result
5. Common Pitfalls & Traps
Pitfall #1: Assuming every frame drop is caused by heavy build() calculations
Remedy: Always check the Flutter DevTools Performance overlay. If the Raster (GPU) bar is red while UI (CPU) is green, the issue is excessive saveLayer passes, opacity, or repainting—not Dart code.
Pitfall #2: Wrapping every single widget in RepaintBoundary indiscriminately
Remedy: RepaintBoundary allocates an offscreen GPU texture pixel buffer. Excessive boundaries waste memory. Apply them only where an actively animating widget is overlaid on static content.
Pitfall #3: Using Opacity(opacity: 0.5, child: ...) on simple single-color shapes
Remedy: The Opacity widget creates an expensive intermediate raster layer. For solid colors, always use Color.withOpacity() or BoxDecoration color alpha instead.