Engineering

Flutter Optimization: The Journey of Developing exping

Kunlin·Aug 1, 2022·23 min read

Over the past six months, I've been immersed in developing exping using Flutter. Upon its release, the app quickly gained traction, securing a spot on the Apple Store's recommended list within its first week.

App Store 本周推荐 exping 截图

While we're incredibly grateful for the warm reception, it also brought to light some areas for improvement. Users reported issues such as page browsing lag, frame drops, and we received feedback along the lines of

Flutter's performance is really subpar compared to native apps.

Our initial choice of Flutter was driven by its cross-platform capabilities, which significantly reduced our development costs. With just two developers handling both front-end and back-end (and everything in between), we were able to bring our vision to life.

Now that we've launched and gained a loyal user base, it's time to roll up our sleeves and address these issues head-on. After all, our careers depend on it!


Tackling First-Run Performance Issues

The most frequent feedback we received was about various page lags. Interestingly, our product, testing, and development teams were puzzled: "We didn't notice any lag..." 😅

After in-depth discussions with our power users, we realized the lag was primarily occurring on the first app launch, with subsequent uses running smoothly. It seems we had unknowingly acclimated to the initial lag 🥶.

This issue, as per the official Flutter documentation, is related to SkSL shader compilation lag.

When an app runs for the first time and enters a page, it needs to compile the shaders used by that page before rendering. To resolve this, we need to implement shader preheating (pre-compilation).

Flutter's official website provides a detailed solution: Shader compilation jank

来自Flutter官网的对比图

Implementation Steps

  1. Run with the --cache-sksl flag to capture shaders (execute directly from the command line):
flutter run --profile --cache-sksl
  1. After running, navigate through all pages in the app that experience lag. For pages with numerous elements or complex animations, it's advisable to open them multiple times.

  2. Press the capital 'M' key (ensure it's uppercase) after opening each page. This action generates a flutter_01.sksl.json file in your project directory, which records the captured shaders.

  3. Modify the run command to include the shader file in the app package. This informs the app which shaders require preheating, enabling shader compilation upon first run before entering the app:

# For running
flutter run --profile --bundle-sksl-path flutter_01.sksl.json

# For building
flutter build apk --bundle-sksl-path flutter_01.sksl.json

After uninstalling the original app and installing the new version, the improvement was immediately noticeable. We shared it with our product team and received enthusiastic approval 👍🏻.

While this solution significantly enhanced the user experience, it does come with some trade-offs:

  • A slight increase in app size due to the additional JSON file. However, the increase (around 1MB) is generally acceptable.

  • A delay of 3-5 seconds when opening the app for the first time if all pages are covered. However, this is a worthwhile trade-off compared to experiencing lag on every page.


Addressing Lag on Specific Pages

Even after implementing the above optimization, some pages still exhibited lag when accessed. After ruling out shader-related issues, we turned our attention to our codebase.

Using DevTools for analysis, we noticed a common pattern among the laggy pages: they were performing network requests or data assembly in the initState method, followed by triggering a page refresh.

When this logic is complex or triggers frequent Widget refreshes, it can cause severe frame drops. Despite Dart being single-threaded, even asynchronous operations can cause page lag if they're too time-consuming (exceeding 16ms under 60Hz refresh rate).

The solution is straightforward - avoid complex operations directly in initState:

/// Perform operations in the next frame
WidgetsBinding.instance!.addObserver((_){
	// ... Specific operations
});

After applying this adjustment to the relevant pages, we observed a significant reduction in frame drops when entering pages.

注意左边顶部进入瞬间卡顿~

To further enhance the user experience, we addressed the abrupt transition from loading screens. We implemented a smooth fade transition:

AnimatedSwitcher(
	duration: const Duration(milliseconds: 200),
	child: child,
	transitionBuilder: (Widget child,Animation<double> animation) {
	  // Opacity transition
	  return FadeTransition(opacity: animation,child: child);
});
Before and after comparison of the transition (GIF speed reduced by 0.8 for clarity)

Optimizing the Photo Album Selector

We received feedback from a user who was unable to load their photo album. Upon inquiry, we discovered they had around 100,000 photos in their device gallery. This presented a significant challenge for our app.

We use the photo_manager plugin for image operations: photo_manager

photo_manager | Flutter Package

Our album selector is based on wechat_assets_picker , customized to better suit our app's needs:

https://github.com/iFREEGROUP/photo_picker

Through debugging, we found that when dealing with around 50,000 photos, PhotoManager.getAssetPathList was taking about 10 seconds to execute. This delay was even longer for photos stored in iCloud.

We discovered that the default parameters (hasAll = true, onlyAll = false) in PhotoManager.getAssetPathList were causing it to wait for all album paths to be retrieved before returning. By setting onlyAll to true, we could retrieve only the most recent photos' album path in under 500ms.

We modified our code accordingly:

// Prioritize getting "All", accelerate photo display
pathList = await PhotoManager.getAssetPathList(
  hasAll: true,
  onlyAll: true,
  type: config.requestType,
  filterOption: options,
);

if (!config.onlyAll) {
  // Then asynchronously get other folders
  PhotoManager.getAssetPathList(
    hasAll: false,
    onlyAll: false,
    type: config.requestType,
    filterOption: options,
  ).then((value) {
    config.getPhotoSortPathDelegate.sort(value);
    _allPathList.addAll(value);
    _cacheFirstThumbFromPathEntity(_allPathList);
  });
}

This approach prioritizes displaying the most recent photos quickly, then asynchronously retrieves other albums.

While it doesn't speed up the retrieval of other albums, it significantly improves the user experience by reducing initial wait times. The background loading of additional albums is imperceptible to the user.

fter optimizing the album opening speed, we tackled the photo list lag issue.

DevTools revealed that loading 40 photos (4x10 grid) in a single frame was triggering a red warning:
DevTools screenshot showing the warning

To address this, we implemented a frame-splitting technique using a community-developed plugin:

keframe | Flutter Package

Post-implementation, DevTools showed a marked improvement in loading speed:

enter image description here

These two optimizations significantly enhanced the album selector's performance. We also applied the frame-splitting loading technique to other pages with numerous complex elements, effectively reducing frame drop occurrences.


Additional Optimizations

Beyond the major improvements discussed, we also refined various aspects of our codebase:

  • Encapsulated popup dialogs for consistency and improved performance
  • Rewrote the emoji rating selector for better user interaction
  • Adjusted the refresh display rules for map markers
  • And several other minor tweaks and improvements

Addressing High Refresh Rate on iPhones

The current Flutter master branch now supports high refresh rates. We're exploring this by adjusting our Flutter version branch. We plan to update our app as soon as Flutter releases a stable version with this feature.
https://github.com/flutter/flutter/issues/90675


We're excited to announce that exping version 1.0.2 is now available, incorporating all these optimizations. We invite you to download and experience the improved performance firsthand at https://exping.world

This journey of optimization has not only enhanced our app's performance but also deepened our understanding of Flutter's intricacies.

We're committed to continual improvement and look forward to bringing an even better experience to our users in future updates.


Share