Flutter App Freeze Debugging: A Step-by-Step Guide
A Flutter app freeze — where the UI stops responding for more than a few frames — is always caused by blocking the main isolate. Flutter runs UI rendering, input handling, and your app logic on the same isolate by default. When that isolate is busy doing heavy work, the UI can't update and the app appears frozen.
This guide covers how to diagnose what's blocking your main isolate and how to fix each common pattern.
Types of Flutter Freezes
Short freeze (100–500ms): A single operation blocking the main isolate briefly. Causes visible jank — animations stutter, scrolling hitches.
Long freeze (500ms–5s): A blocking operation that takes long enough for users to notice the app is unresponsive. Often triggers an ANR on Android.
Permanent freeze: An infinite loop or deadlock. The app never recovers and must be force-killed.
ANR (Application Not Responding) on Android triggers when the main thread is blocked for more than 5 seconds. iOS doesn't have an equivalent but users will force-quit unresponsive apps.
Step 1: Detect Freezes with Flutter DevTools
Flutter DevTools Performance tab shows frame rendering time. Any frame taking more than 16ms (for 60fps) causes jank. Frames over 100ms cause visible freezes.
1. Run your app in profile mode: flutter run --profile
2. Open DevTools: flutter pub global run devtools
3. Go to the Performance tab
4. Interact with the UI and watch for red/yellow frames
5. Click a slow frame to see the timeline breakdown
The timeline will show you exactly which function call consumed the frame budget.
Step 2: Common Blocking Patterns and Fixes
JSON parsing large datasets on the main isolate
// WRONG — blocks main isolate for large payloads
final response = await http.get(uri);
final data = jsonDecode(response.body); // can take 50-200ms for large JSON
setState(() => items = data['items']);
// CORRECT — parse in a background isolate
import 'package:flutter/foundation.dart';
final response = await http.get(uri);
final items = await compute(parseItems, response.body);
setState(() => _items = items);
// Top-level function (required for compute())
List<Item> parseItems(String body) {
final data = jsonDecode(body) as Map<String, dynamic>;
return (data['items'] as List).map((e) => Item.fromJson(e)).toList();
}Image processing on main isolate
// WRONG — image manipulation blocks main isolate
final bytes = await file.readAsBytes();
final image = await decodeImageFromList(bytes); // can be slow for large images
// CORRECT — use an isolate for heavy image work
import 'package:flutter_isolate/flutter_isolate.dart';
final processedBytes = await flutterCompute(processImage, bytes);SQLite / database queries on main isolate
SQLite (via sqflite) is synchronous by default if you use the wrong API. Always use async methods:
// WRONG — synchronous database query blocks UI
final db = await openDatabase('app.db');
final results = db.rawQuerySync('SELECT * FROM items'); // blocks!
// CORRECT — async database operations
final results = await db.rawQuery('SELECT * FROM items'); // non-blockingFile I/O on main isolate
// WRONG — synchronous file read blocks main thread
final content = File(path).readAsStringSync();
// CORRECT — async file operations
final content = await File(path).readAsString();Heavy computation (sorting, filtering large lists)
// WRONG — sorting 10,000 items on main isolate
final sorted = items.sorted((a, b) => a.price.compareTo(b.price));
// CORRECT — use compute() for CPU-intensive work
final sorted = await compute(sortItems, items);
List<Item> sortItems(List<Item> items) {
return [...items]..sort((a, b) => a.price.compareTo(b.price));
}Step 3: Add Freeze Detection to Production Monitoring
You can detect UI freezes in production using the SchedulerBinding frame callback:
import 'package:flutter/scheduler.dart';
import 'package:bugspulse/bugspulse.dart';
class FreezeDetector {
static void init() {
SchedulerBinding.instance.addTimingsCallback((timings) {
for (final timing in timings) {
final frameDuration = timing.totalSpan.inMilliseconds;
if (frameDuration > 200) {
// Frame took >200ms — report as a performance event
BugsPulse.addBreadcrumb(
message: 'Slow frame: ${frameDuration}ms',
level: BreadcrumbLevel.warning,
data: {'frame_duration_ms': frameDuration},
);
}
}
});
}
}
// Call in main()
FreezeDetector.init();Step 4: Use Isolates for Background Work
Flutter's Isolate API runs Dart code in a separate thread with its own memory. Use it for any CPU-intensive work:
import 'dart:isolate';
Future<List<ProcessedItem>> processInBackground(List<RawItem> items) async {
final receivePort = ReceivePort();
await Isolate.spawn(
_processItems,
[receivePort.sendPort, items],
);
return await receivePort.first as List<ProcessedItem>;
}
void _processItems(List<dynamic> args) {
final sendPort = args[0] as SendPort;
final items = args[1] as List<RawItem>;
final result = items.map(processItem).toList();
sendPort.send(result);
}For simpler cases, Flutter's compute() function wraps this pattern automatically.
Summary
Flutter freezes are always main-isolate blocking. Profile first with DevTools to identify the exact function causing the slowdown. Move heavy work — JSON parsing, file I/O, database queries, image processing, sorting large lists — to background isolates with compute() or Isolate.spawn(). Add frame timing callbacks in production to detect regressions before users report them.