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
Developers often waste days prematurely refactoring build() methods when animations stutter, assuming Dart CPU code is slow. In reality, modern Dart executes build passes in microseconds. The actual culprit for dropped frames (jank) is frequently the Raster Thread (GPU pipeline), caused by expensive off-screen saveLayer passes, dynamic blur filters (BackdropFilter), unclipped path antialiasing, or continuous repainting of static background trees during localized ticker animations.
2. Architectural Principles & Resolution
Diagnosing performance requires understanding Flutter's two distinct execution threads: 1. UI Thread (Dart VM / CPU): - Executes build() methods, layout passes, and animation tweens. - Diagnosed via DevTools CPU Profiler and Timeline flame charts. - Fixed by: Using const constructors, caching expensive calculations outside build(), and using targeted builder widgets (e.g. AnimatedBuilder). 2. Raster Thread (GPU / Impeller / Skia): - Renders drawing commands into GPU pixels. - Diagnosed via "Track Raster Thread" and "Highlight Repaints" flags in DevTools. - Bottlenecks include: Opacity widgets wrapping complex subtrees (forces expensive saveLayer), BackdropFilter blurs, and unbounded path clippings. - Fixed by: Wrapping actively animating widgets in a RepaintBoundary to prevent repainting static adjacent subtrees, and using Color.withOpacity or AnimatedOpacity instead of general-purpose Opacity widgets.
3. Complete 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
Enabling "Highlight Repaints" in Flutter DevTools shows only the center spinning box outlined in rotating color flashes. The 50 background grid items remain completely untouched, keeping raster thread time under 2ms per frame (rock-solid 60/120 FPS).
5. Common Pitfalls & Traps
Pitfall #1: Assuming every frame drop is caused by heavy build() calculations
Pitfall #2: Wrapping every single widget in RepaintBoundary indiscriminately
Pitfall #3: Using Opacity(opacity: 0.5, child: ...) on simple single-color shapes