Monitor Performance of your Flutter App with DevTools: Enhance Tracing

Trace widget builds add an event to the timeline for each widget that is built. Let’s take default counter app.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter App',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.blue),
home: const MyHomePage(title: 'Counter Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({super.key, required this.title});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Run the app in Profile mode, go to the Performance tab, and enable Trace widget builds.
Now, whenever we click on the FloatingActionButton, we see a number of frames due to the animation in the button. Only the first frame contains information about the widget builds; the rest of the frames only show data related to painting. So, clear any existing frames, click on the FloatingActionButton in the app, and select the first frame.
We can see that the build took 1.3ms. If you're curious, you can check other frames, but they do not contain any details about build time.
So let's go to the timeline events for the first frame. Since we have enabled trace widget builds, we should see the names of the widgets built in that frame. Sometimes, when I check the timeline tab, I see nothing. If this happens to you, just click the reload button at the top right of the timeline event tab or try selecting another frame and then come back again.
We can click on each event added to the timeline to get more details. Let's click on the build event to see what information it provides.
We can see that the build duration was 1 millisecond and 275 microseconds, which was rounded to 1.3ms in the Frame Analysis tab. We can use this to identify which widget has costly build functions.
Now let’s look into an app with slow build function.
class SlowBuildScreen extends StatefulWidget {
const SlowBuildScreen({super.key});
@override
State<SlowBuildScreen> createState() => _SlowBuildScreenState();
}
class _SlowBuildScreenState extends State<SlowBuildScreen> {
bool _isLoading = false;
int _fiboNumber = 35;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isLoading) const CircularProgressIndicator(),
SlowBuild(
number: _fiboNumber,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
setState(() {
_isLoading = true;
_fiboNumber++;
});
await Future.delayed(const Duration(milliseconds: 1500));
setState(() {
_isLoading = false;
});
},
child: const Icon(Icons.add),
),
);
}
}
class SlowBuild extends StatelessWidget {
const SlowBuild({
super.key,
required this.number,
});
final int number;
int _fibo(int n) {
if (n <= 1) {
return 1;
}
return _fibo(n - 1) + _fibo(n - 2);
}
@override
Widget build(BuildContext context) {
final result = _fibo(number);
return Text("fibonicci($number): $result");
}
}
As we can see, due to the slow build function, there is a lag in the loading animation. Let's check this in the DevTools performance tab with the trace widget build option enabled.
Frame Analysis
In frame 234, we can see that the build took 156.8 milliseconds. The suggestion is to check the timeline events.
The length of the event shows how long it took. We can see that the SlowBuild widget is causing the slow build, which also slows down its parent widget. Let's click on the SlowBuild widget to see what details we can find.
The SlowBuild function took 156 milliseconds and 291 microseconds to build, which is causing the jank. To improve this, we can use the compute function to run our Fibonacci logic in a separate thread, similar to how we call any asynchronous functions. The updated code would be as follows:
class FastBuildScreen extends StatefulWidget {
const FastBuildScreen({super.key});
@override
State<StatefulWidget> createState() => _FastBuildScreenState();
}
class _FastBuildScreenState extends State<FastBuildScreen> {
final ValueNotifier<int> _fiboNumberNotifier = ValueNotifier(35);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ValueListenableBuilder(
valueListenable: _fiboNumberNotifier,
builder: (context, value, child) {
return FastBuild(
number: value,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_fiboNumberNotifier.value++;
},
child: const Icon(Icons.add),
),
);
}
}
class FastBuild extends StatelessWidget {
const FastBuild({
super.key,
required this.number,
});
final int number;
int _fibo(int n) {
if (n <= 1) {
return 1;
}
return _fibo(n - 1) + _fibo(n - 2);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: compute(_fibo, number), // This will execute the _fibo function in a separate thread.
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
children: [
const CircularProgressIndicator(),
Text("fibonicci($number): -----")
],
);
}
if (snapshot.hasError) {
return const Text("Some error occurred");
}
return Text("fibonicci($number): ${snapshot.data}");
},
);
}
}
Let's take a look at the results after making the changes.
This time, we don't get a janky frame.
To summarize, we used Trace widget builds to monitor every widget that was built, including details like the number of widgets built in a frame and the time each one took to build. For a janky frame, we identified which widget caused the issue. To fix the jank, we found out what costly operation our build was performing. Since our build was doing a costly synchronous operation, we used the compute function to run the function separately, ensuring our UI thread runs smoothly.
Subscribe to my newsletter
Read articles from Nitin Poojary directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
