Firebase Performance offers a free, comprehensive solution for tracking app performance. As part of the Firebase suite, it provides seamless integration with other Firebase services like Crashlytics, making it easier to manage all performance and crash data in one place. This not only simplifies access but also streamlines team collaboration without the need to manage multiple platforms.
Below, I will dive into the key features of Firebase Performance for Flutter applications and demonstrate how it can be used for monitoring app performance.
Integrating Firebase Performance in a Flutter App
Integrating Firebase Performance into your Flutter application is a straightforward process, especially if you have already set up Firebase. If you haven’t configured Firebase yet, you can refer to the documentation for detailed guidance on setting it up for Flutter projects.
Once Firebase is configured, you simply need to add the following dependency to your pubspec.yaml
file:
dependencies:
firebase_performance: ^0.10.0+8
Next, enable performance collection by calling FirebasePerformance.instance.setPerformanceCollectionEnabled(true)
in your main.dart
file.
After completing these steps, you are ready to trace your network requests and custom events, unlocking the full potential of Firebase Performance for monitoring your application.
Monitoring Automatic Performance Metrics
Firebase Performance automatically collects a variety of metrics that can be invaluable for developers, such as network traffic, app startup time, and time spent in the background and foreground. Additionally, it tracks screen rendering, providing insights into slow rendering events (those taking longer than 16ms) and freezes (that taking longer than 700ms).
While these features are highly useful, there are some limitations to consider when using Firebase Performance with Flutter applications.
Firstly, the app startup time metric only measures the native part of the application, meaning that it does not accurately capture the full startup time, especially the loading of the Flutter portion.
Secondly, the screen rendering metrics also apply solely to native components. While knowing how quickly your main Activity or ViewController loads can be helpful, it’s less relevant for Flutter developers who are more concerned with the performance of Flutter screens.
Unfortunately, as of now, tracking the rendering performance of Flutter screens is not supported. However, further in this article, I will explore how we can gather similar performance data specific to Flutter screens.
Network Request Monitoring
Although network traffic is not automatically tracked by Firebase Performance when using dio
, adding this functionality is straightforward. You can either write your own interceptor or use an existing one from pub.dev. Additionally, if you have a custom wrapper around dio
, you can easily integrate request tracing into your existing architecture.
Assuming you have a wrapper and all network calls are made through the request method of dio
, the implementation might look as follows:
class DioWrapper {
DioWrapper(this._dio);
final Dio _dio;
Future<Response> request(String path, {required RequestOptions options}) async {
final HttpMetric metric = FirebasePerformance.instance.newHttpMetric(
options.uri.toString(),
HttpMethod.Get, // Set the right HTTP method
);
metric.start();
try {
final response = await _dio.request(path, options: options);
metric
..httpResponseCode = response.statusCode
/**
..requestPayloadSize = requestSize
..responsePayloadSize = responseSize
..responseContentType = contentType;
*/
return response;
} catch (e) {
rethrow;
} finally {
metric.stop();
}
}
}
Using Custom Traces
In my opinion, this is one of the most valuable features of Firebase Performance. Identifying bottlenecks in your code isn’t always straightforward, especially when working on high-end devices, which may mask performance issues. However, Firebase custom traces allow you to track how long specific operations take on actual user devices, offering insights into real-world performance. Creating a trace is similar to how we create an HttpMetric
.
Here’s an example of using a custom trace to monitor the performance of a time-consuming operation:
import 'package:firebase_performance/firebase_performance.dart';
void performHeavyOperation() async {
final trace = FirebasePerformance.instance.newTrace("heavy_operation_trace");
await trace.start();
await Future.delayed(Duration(seconds: 3));
await trace.stop();
}
This allows you to capture how long the operation takes on different devices. When these traces are collected across your user base, you can see how much time it takes on average or for specific users, giving you insights into potential performance issues.
How can custom metrics help?
Consider a scenario where you are handling a massive JSON file for localization (why not?). You retrieve it from the backend, parse it, and then use the localized strings in the app. The parsing process likely occurs on the UI thread, which can cause frame drops or even give users the impression that the app has frozen. In this case, it’s essential to track how long the parsing process takes and monitor any changes in this duration over time.
Custom traces allow you to measure the parsing time, so you can detect whether this operation is becoming a performance bottleneck. Here’s an example:
void parseLargeJson(String jsonString) {
final trace = FirebasePerformance.instance.newTrace("parse_localization_json");
race.start();
final parsedJson = jsonDecode(jsonString);
trace parseTrace.stop();
}
By using Firebase Performance’s custom traces, you can monitor and improve performance-critical sections of your code, ensuring that operations run efficiently across a wide range of devices, improving the overall user experience.
Detecting Dropped Frames and Diagnosing Freezes
As mentioned earlier, Firebase provides native apps with the ability to detect freezes and dropped frames, but unfortunately, this functionality isn’t directly available for Flutter applications. It would be beneficial to have such a feature for Flutter apps as well. While it won’t be as polished as the native implementation, we can still create something useful.
Flutter offers a way to access frame rendering information not only through DevTools
, but also by adding a callback that provides detailed frame timing information:
SchedulerBinding.instance.addTimingsCallback((timings) {
for (final timing in timings) {
print('[${timing.frameNumber}] Build took ${timing.buildDuration.inMilliseconds}ms, Raster took ${timing.rasterDuration.inMilliseconds}ms');
}
});
The callback will provide a list of timing objects, from which you can access timing.buildDuration
and timing.rasterDuration
. If either of these stages takes longer than 16ms (for a 60Hz display), you have a janky frame.
For example, as shown in the screenshot from DevTools
, frames 25 and 26 exceed the 16ms threshold, and you’ll see the same information reflected in the logs.
I/flutter (10272): [21] Build took 0ms, Raster took 17ms
I/flutter (10272): [22] Build took 0ms, Raster took 21ms
I/flutter (10272): [23] Build took 0ms, Raster took 16ms
I/flutter (10272): [24] Build took 0ms, Raster took 10ms
I/flutter (10272): [25] Build took 1ms, Raster took 24ms
I/flutter (10272): [26] Build took 3ms, Raster took 33ms
Having detailed rendering information enables us to analyze performance and address weak points that may be affecting the app’s responsiveness. To tackle this, we can leverage the capabilities of both Firebase Performance and Firebase Crashlytics. By combining these tools, we can not only track rendering issues but also gain valuable context to make informed improvements.
Logging to Firebase Performance
DThe first option is to create a custom trace in Firebase Performance to log this information. However, there’s a challenge: Firebase Performance does not allow you to create a trace with a specified start and end time. We could implement a workaround like this:
static void _logWithTime(Duration d, String name) async {
final trace = FirebasePerformance.instance.newTrace(name);
trace.start();
await Future.delayed(d);
trace.stop();
}
But even this approach has limitations. There’s no guarantee that trace.stop()
will be called exactly after Duration d
—for example, if a large JSON file is being parsed at that moment. Even if we accept this inaccuracy, another issue arises: Firebase Performance won't provide additional information about the trace, such as which screen the user was on or what actions led up to the janky frame. This leaves us with minimal insight—just that a freeze occurred somewhere in the app.
The second one is to create a custom trace in Firebase Performance to log this information. You could even save all rendering data, not just those that exceed 16ms. This approach would allow you to gather comprehensive insights into how long rendering takes on users’ devices, helping you analyze the distribution and determine the percentage of frames that take less than 16ms. As a general performance indicator, this would be a valuable metric for understanding overall app responsiveness. You might log the following data to Firebase Performance:
for (final timing in timings) {
final trace = AppTracing.createTrace("frame_rendering");
await trace.start();
trace.setMetric("build_duration", timing.buildDuration.inMilliseconds);
trace.setMetric("raster_duration", timing.rasterDuration.inMilliseconds);
await trace.stop();
}
As seen in the screenshot above, the 90th percentile of frames shows that rasterization takes only 3ms, while building takes 9ms for the same percentile. This provides a convenient way to monitor the overall performance of your application and helps in identifying potential areas for optimization.
Logging to Firebase Crashlytics
What if we log this data in Firebase Crashlytics instead? Since Crashlytics likely already logs analytics events, screen transitions, and other context, it could be easier to pinpoint the problematic area. Additionally, logging non-fatal errors in Crashlytics won’t affect your crash-free rate. Here’s how we log that data:
for (final timing in timings) {
if (timing.totalSpan.inMilliseconds > 16) {
FirebaseCrashlytics.instance.recordError(
Exception('JankFrame'),
null,
information: [timing.toString()],
printDetails: true,
fatal: false,
);
}
}
That logs janky frames directly into Crashlytics as non-fatal errors. Because Crashlytics already collects user sessions and screen transition events, you can cross-reference this data to identify what led to the performance issues.
For instance, after adding logging to Crashlytics, you might find that building MigraineDetailsPag
takes too long. This information allows you to take actionable steps to enhance user experience (UX), rather than simply knowing there are rendering issues in the app.
Conclusion
In summary, leveraging Firebase Performance in conjunction with Crashlytics for monitoring your Flutter application’s performance can significantly enhance your ability to identify and address bottlenecks. With insights into user behavior and performance patterns, you can iteratively improve your app’s responsiveness while maintaining high standards of user satisfaction.
As you continue to optimize your Flutter applications, consider incorporating Firebase Performance into your performance monitoring strategy. This not only helps you track essential metrics but also empowers you to create a smoother and more enjoyable experience for your users.