Performance Optimization

Detecting and Eliminating Jank

16 min Lesson 9 of 12

Detecting and Eliminating Jank

Smooth animation in Flutter requires rendering each frame in under 16 milliseconds — the budget for a 60 fps display. Any frame that takes longer causes a visible stutter known as jank. Users notice jank immediately: scrolling feels sluggish, animations stutter, and interactions feel unresponsive. This lesson teaches you what jank is at the engine level, how to reproduce it reliably, and how to track down its root cause using Flutter DevTools.

What Is Jank?

Flutter's rendering pipeline processes every frame in two parallel threads: the UI thread (Dart isolate — builds the widget tree and layout) and the raster thread (GPU — paints pixels). Both threads share the 16 ms frame budget. If either thread misses its deadline, the GPU compositor cannot swap the buffer in time and the display repeats the previous frame, producing a visible dropped frame.

  • UI thread jank — caused by heavy synchronous work in build(), setState(), or layout computation.
  • Raster thread jank — caused by expensive GPU operations such as shader compilation, large saveLayer() calls, or complex ClipPath compositing.
  • Platform thread jank — caused by expensive work on the platform channel (e.g., plugin calls blocking the main thread).
Note: Flutter targets 60 fps (16 ms/frame) on most devices and 120 fps (8 ms/frame) on ProMotion displays. The DevTools frame bar colour-codes frames: green = on time, yellow = slightly over budget, red = severely janked.

Reproducing Jank Reliably

You cannot measure performance in debug mode — the Dart VM's JIT compiler and enabled assertions inflate frame times by 3–10×. Always profile in profile mode:

# Run on a physical device in profile mode
flutter run --profile

# Or build a profile APK/IPA
flutter build apk --profile
flutter build ios --profile

Profile mode disables debug assertions and enables AOT compilation, giving results representative of what end users experience. Use a mid-range physical device — simulators and emulators do not accurately reproduce GPU constraints.

To reproduce jank deterministically, create a minimal test case that isolates the slow path. A common technique is to build a list with intentionally expensive build() methods:

// Intentionally janky list — never do this in production
class JankyList extends StatelessWidget {
  const JankyList({super.key});

  String _heavyCompute(int index) {
    // Simulates blocking synchronous work on the UI thread
    var result = 0;
    for (var i = 0; i < 500000; i++) {
      result += i * index;
    }
    return 'Item $index: $result';
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 200,
      itemBuilder: (context, index) {
        // _heavyCompute runs SYNCHRONOUSLY inside build — classic jank source
        final label = _heavyCompute(index);
        return ListTile(title: Text(label));
      },
    );
  }
}
Warning: Never run performance profiling with flutter run (debug mode). The results will be completely misleading. Always use flutter run --profile or flutter run --release.

Opening the Timeline in Flutter DevTools

Flutter DevTools' Performance tab contains the Timeline view and the Frame Rendering bars — the two primary tools for diagnosing jank.

  • Start your app in profile mode: flutter run --profile
  • Open DevTools from the terminal URL printed by Flutter, or via flutter pub global activate devtools && flutter pub global run devtools
  • Click the Performance tab and press Record
  • Reproduce the janky interaction on the device
  • Press Stop and inspect the captured trace

Reading the Frame Rendering Bars

The top of the Performance tab shows a bar chart — one bar per rendered frame, split into a blue UI-thread half and a green raster-thread half. The red horizontal line marks the 16 ms deadline.

  • A bar whose blue portion crosses the red line indicates UI-thread jank — look for expensive work in build() or layout().
  • A bar whose green portion crosses the line indicates raster-thread jank — look for saveLayer, heavy compositing, or first-frame shader compilation.
  • Click any tall bar to load its flame chart in the Timeline below. The flame chart shows the exact Dart call stack, so you can see which function consumed the most time.
Tip: Use the Enhance Tracing menu in the Performance tab to enable extra timeline events for widget builds, layouts, and paints. These are disabled by default because they add some overhead, but they pinpoint exactly which widget is slow.

Eliminating UI-Thread Jank

Once you identify a hot function in the flame chart, apply one of these standard fixes:

  • Move work off the UI thread — use compute() or an Isolate for CPU-bound tasks (JSON decoding, image processing, sorting large lists).
  • Cache expensive results — use const constructors and memoisation so build() does not recompute on every frame.
  • Narrow rebuilds — split large widgets into smaller ones, or use RepaintBoundary to isolate subtrees that change frequently.
  • Avoid synchronous I/O — all file, network, and database operations must be asynchronous (Future / async-await).
import 'package:flutter/foundation.dart'; // for compute()

// Move heavy work to a background isolate
Future<String> _heavyComputeIsolate(int index) {
  return compute(_isolateWork, index);
}

String _isolateWork(int index) {
  var result = 0;
  for (var i = 0; i < 500000; i++) {
    result += i * index;
  }
  return 'Item $index: $result';
}

// Fixed version: build() is now instant
class SmoothList extends StatelessWidget {
  const SmoothList({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 200,
      itemBuilder: (context, index) {
        return FutureBuilder<String>(
          future: _heavyComputeIsolate(index),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return const ListTile(title: Text('Loading...'));
            }
            return ListTile(title: Text(snapshot.data!));
          },
        );
      },
    );
  }
}

Summary

Jank occurs when a frame takes longer than 16 ms to complete on either the UI or raster thread. To diagnose it: run in profile mode, open the DevTools Performance tab, record while reproducing the problem, and inspect the frame bars and flame chart to find the slow call stack. Fix UI-thread jank by offloading CPU work to isolates, caching results, narrowing widget rebuilds, and avoiding synchronous blocking calls. Raster-thread jank (covered in a later lesson) requires avoiding excessive saveLayer usage and enabling the Impeller renderer for shader-compilation jank.