Skip to main content
Back to all tutorials
Tested: Flutter 3.24.3 • Dart 3.5.3
T15.01 Verified September 2026
Performance Jank DevTools Profiling RepaintBoundary const

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 Tested Flutter Code

main.dart (Flutter 3.x+ ready)
lib/main.dart Copy & Paste into a new Flutter project
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

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.

Official Flutter References & Standards