How to check jank in Flutter and how to avoid it in future?

#flutter#performance#jank#fps#optimization#debugging

Answer

Overview

Jank is any visible stutter, freeze, or jerky motion in the UI caused by frames that take longer than 16.67ms (at 60fps) to render. When a frame misses its deadline, the previous frame is displayed again, making animations and scrolling appear choppy.


How to Detect Jank

1. Performance Overlay

bash
flutter run --profile
# Press P to toggle the overlay
  • Red bars in bottom graph (UI thread) = Dart code is too slow
  • Red bars in top graph (Raster thread) = Rendering is too complex
  • Any bar exceeding the white 16ms line = dropped frame = jank

2. DevTools Performance Tab

text
1. Run: flutter run --profile
2. Open DevTools > Performance tab
3. Click "Record" and interact with your app
4. Janky frames appear as tall bars exceeding the 16ms line
5. Click individual frames to see build, layout, paint breakdown
6. Shader compilation jank appears in dark red

3. Programmatic Detection

dart
import 'package:flutter/scheduler.dart';

void detectJank() {
  SchedulerBinding.instance.addTimingsCallback((
    List<FrameTiming> timings,
  ) {
    for (final timing in timings) {
      final totalMs = timing.totalSpan.inMilliseconds;
      if (totalMs > 16) {
        debugPrint('JANK! Frame took ${totalMs}ms '
          '(build: ${timing.buildDuration.inMilliseconds}ms, '
          'raster: ${timing.rasterDuration.inMilliseconds}ms)');
      }
    }
  });
}

Always profile on a real device in profile mode. Debug mode and emulators give misleading results.


Common Causes and How to Fix

Cause 1: Heavy Computation on Main Thread

dart
// ❌ BAD — Blocks UI thread for 200ms
void loadData() {
  final result = heavyJsonParsing(rawData); // Blocking!
  setState(() => data = result);
}
dart
// ✅ GOOD — Offload to Isolate
import 'package:flutter/foundation.dart';

Future<void> loadData() async {
  // compute() runs in a separate Isolate
  final result = await compute(heavyJsonParsing, rawData);
  setState(() => data = result);
}

// Or using Isolate.run (Dart 2.19+)
Future<void> loadData() async {
  final result = await Isolate.run(() {
    return heavyJsonParsing(rawData);
  });
  setState(() => data = result);
}

Cause 2: Excessive Widget Rebuilds

dart
// ❌ BAD — Entire list rebuilt on every frame
ListView(
  children: items.map((item) => ExpensiveWidget(item: item)).toList(),
);

// ✅ GOOD — Lazy building, only visible items built
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ExpensiveWidget(
    item: items[index],
  ),
);
dart
// ❌ BAD — setState rebuilds everything
class _DashboardState extends State<Dashboard> {
  int counter = 0;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        HeavyChart(),        // Rebuilds unnecessarily!
        HeavyHeader(),       // Rebuilds unnecessarily!
        Text('$counter'),
        ElevatedButton(
          onPressed: () => setState(() => counter++),
          child: const Text('+'),
        ),
      ],
    );
  }
}
dart
// ✅ GOOD — const widgets skip rebuild
class _DashboardState extends State<Dashboard> {
  int counter = 0;

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        const HeavyChart(),   // Skipped! (const)
        const HeavyHeader(),  // Skipped! (const)
        Text('$counter'),
        ElevatedButton(
          onPressed: () => setState(() => counter++),
          child: const Text('+'),
        ),
      ],
    );
  }
}

Cause 3: Repaint Scope Too Large

dart
// ✅ GOOD — Isolate frequently repainting widgets
RepaintBoundary(
  child: AnimatedWidget(
    // Only this subtree repaints on animation tick
    // Rest of the screen is NOT repainted
  ),
)

Cause 4: Shader Compilation Jank

First-time shader compilation can take 20-200ms per shader.

Fix Option A: Use Impeller (recommended)

Impeller pre-compiles all shaders AOT, eliminating runtime shader jank entirely.

bash
# Impeller is default on iOS (Flutter 3.16+) and Android (Flutter 3.22+)
# To explicitly enable:
flutter run --enable-impeller

Fix Option B: SkSL Warm-Up (for Skia backend)

bash
# Step 1: Capture shaders during a test run
flutter run --profile --cache-sksl --purge-persistent-cache

# Step 2: Exercise ALL animations in the app

# Step 3: Press M in terminal to export shaders
# Saves to: flutter_01.sksl.json

# Step 4: Build with shader bundle
flutter build apk --bundle-sksl-path flutter_01.sksl.json
flutter build ios --bundle-sksl-path flutter_01.sksl.json

Cause 5: Expensive saveLayer Operations

dart
// ❌ BAD — Opacity triggers saveLayer on entire subtree
Opacity(
  opacity: 0.5,
  child: ComplexWidgetTree(), // Expensive saveLayer!
)

// ✅ GOOD — Use AnimatedOpacity or FadeTransition
AnimatedOpacity(
  opacity: 0.5,
  duration: const Duration(milliseconds: 300),
  child: ComplexWidgetTree(),
)

Cause 6: Large Image Decoding

dart
// ❌ BAD — Full-resolution image decoded on main thread
Image.network('https://example.com/4000x3000.jpg')

// ✅ GOOD — Resize at decode time + pre-cache
Image.network(
  'https://example.com/4000x3000.jpg',
  cacheWidth: 400,   // Decode at display size
  cacheHeight: 300,
)

// Pre-cache before navigating
precacheImage(
  NetworkImage('https://example.com/hero.jpg'),
  context,
);

Jank Prevention Checklist

ActionImpact
Use
text
const
constructors wherever possible
High
Use
text
ListView.builder
instead of
text
ListView(children: [...])
High
Offload heavy work to Isolates via
text
compute()
Very High
Wrap animated widgets in
text
RepaintBoundary
Medium
Avoid
text
Opacity
on complex subtrees
Medium
Pre-cache images with
text
precacheImage()
Medium
Use Impeller renderer (default on modern Flutter)Very High
Use
text
cacheWidth
/
text
cacheHeight
for large images
Medium
Always test in profile mode on physical devicesCritical
Avoid synchronous I/O on the main threadHigh

Quick Diagnostic Flow

text
App feels janky?
├─ Run: flutter run --profile
├─ Enable Performance Overlay (press P)
├─ Red bars in BOTTOM graph (UI thread)?
│   ├─ Heavy build() method → simplify, use const
│   ├─ Heavy computation → use compute() / Isolate
│   └─ Too many rebuilds → targeted setState, const widgets
├─ Red bars in TOP graph (Raster thread)?
│   ├─ Shader compilation → enable Impeller or SkSL warm-up
│   ├─ Complex painting → use RepaintBoundary
│   └─ saveLayer (Opacity, ClipPath) → use alternatives
└─ Both graphs red?
    └─ Widget tree too complex → simplify, split into smaller widgets

Golden Rule: Profile early, profile often. Don't wait until the app is complete to check for jank — catch it during development.

Learn more at Flutter Performance Profiling and Shader Compilation Jank.