Flutter Rendering Performance Optimization Practices at Ctrip Train Ticket
This article shares the performance optimization techniques used by Ctrip Train Ticket's Flutter team, covering rendering bottlenecks, selective UI refresh with Provider Selector, granular setState usage, ViewModel splitting, widget caching, const usage, RepaintBoundary, isolate off‑loading, long‑list handling and image loading memory management.
Background
Ctrip Train Ticket has applied Flutter across dozens of core business pages for over a year. The team distilled a set of effective performance‑optimization methods, using performance‑analysis tools to locate, classify and resolve rendering issues, and presents several case studies.
Rendering Optimization
2.1 Control Refresh Scope with Selector
Flutter performance problems appear on the GPU thread or the UI (CPU) thread. When the UI thread chart turns red, heavy Dart code execution is the cause. By combining the Performance Overlay with flame graphs, developers can pinpoint long‑running methods and deep widget layers. Over‑rendering on the GPU often stems from excessive widget composition, missing caches, or unnecessary clipping.
Example: using setState to change the top bar opacity triggers a full rebuild. By moving the opacity value into a ChangeNotifier and updating it via notifyListeners() , only the affected widget rebuilds.
int scrollHeight = 120;
_scrollController.addListener(() {
if (_scrollController.offset > scrollHeight && _titleAlpha != 255) {
setState(() { _titleAlpha = 255; });
}
if (_scrollController.offset <= 0 && _titleAlpha != 0) {
setState(() { _titleAlpha = 0; });
}
if (_scrollController.offset > 0 && _scrollController.offset < scrollHeight) {
setState(() { _titleAlpha = _scrollController.offset * 255 ~/ scrollHeight; });
}
});After refactoring with Selector<TopTabStatusViewModel, int> , only the top bar rebuilds during scrolling, dramatically reducing rebuild cost.
Selector<TopTabStatusViewModel, int>(builder: (context, alpha, child) {
return Container(
color: Colors.white.withAlpha(alpha),
child: Column(children: [HotelDetailNavBar(alpha, widget.pageDeliverData, hotelDetail)]),
);
}, selector: (context, viewModel) => viewModel.titleAlpha);2.2 Reduce setState Granularity
For a carousel that updates text every 2 seconds via a Timer , the whole view was being rebuilt. Encapsulating the carousel into its own widget limits the rebuild to the text widget only.
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(this.texts),
);
}2.3 Decrease Component Re‑draw Frequency
Uncontrolled setState calls cause unnecessary UI refreshes. Adding conditional checks prevents redundant updates when the selected price range does not change.
if (lowerValue != startSortPrice || upperValue != endSortPrice) {
setState(() {
startSortPrice = lowerValue;
endSortPrice = upperValue;
});
refreshPriceText(lowerValue, upperValue);
}2.4 Split ViewModels to Lower Refresh Rate
Instead of a monolithic ViewModel, split responsibilities so each ViewModel only manages a single UI segment. Use MultiProvider at the page entry to supply the distinct ViewModels.
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CalendarSelectorViewModel()),
ChangeNotifierProvider(create: (_) => TopTabStatusViewModel()),
],
child: HotelDetailPageful(scriptDataEntity),
);2.5 Cache High‑Level Widgets
Store frequently used top‑level widgets in a list and reuse them instead of rebuilding on every page refresh.
List
widgets = [];
bool refreshPage = true;
List
getPageWidgets(ScriptDataEntity data) {
if (widgets.isNotEmpty && !refreshPage) return widgets;
// build widgets and cache them
}2.6 Use const to Avoid Re‑creation
Mark immutable widgets with const to prevent repeated construction and reduce garbage‑collection pressure, especially for animated components.
2.7 RepaintBoundary Isolation
Wrap frequently changing widgets such as Swiper, PageView or Lottie animations with RepaintBoundary to isolate their repaint layers.
RepaintBoundary(
child: Container(
child: Lottie.network(InlandPicture.otaLottieJson),
),
);2.8 Avoid ClipPath
ClipPath is expensive; prefer Container with borderRadius for simple rounded corners.
2.9 Reduce Opacity Usage
Replace Opacity with AnimatedOpacity or FadeInImage to avoid rebuilding the whole subtree each frame.
AnimatedOpacity(
opacity: showHeader ? 1.0 : 0.0,
duration: Duration(milliseconds: 200),
child: Container(...),
);Root Isolate Optimization
3.1 Minimize Logic in build()
Keep heavy calculations out of build() ; move them to initState() or guard with state flags to lower CPU usage.
3.2 Off‑load Expensive Work to Isolates
Use Dart isolates for time‑consuming tasks such as real‑time opacity calculation during scrolling, preventing UI thread blockage.
Long‑List Scrolling Optimization
4.1 ListView Item Reuse
Leverage GlobalKey to retain widget state across pagination, avoiding full list rebuilds.
Widget listItem(int index, dynamic model) {
if (listViewModel!.listItemKeys[index] == null) {
listViewModel!.listItemKeys[index] = RectGetter.createGlobalKey();
} else {
final rectGetter = listViewModel!.listItemKeys[index];
if (rectGetter is GlobalKey) {
final widget = rectGetter.currentWidget as RectGetter?;
if (widget != null) return widget;
}
}
}4.2 Home Page Pre‑load
Pre‑load list data on the previous page so the first screen appears instantly.
4.3 Pagination Pre‑load
When the scroll reaches a threshold of remaining items, trigger the next page load to avoid visible waiting.
if (!(itemRect.top > rect.bottom || itemRect.bottom < rect.top)) {
// load next page
}4.4 Cancel In‑flight Network Requests
When a refresh occurs, cancel pending requests identified by a unique token to prevent data inconsistency.
if (isRefresh) {
cancelRequest(identifier);
}
identifier = 'QUERY_IDENTIFIER' + 'timestamp';Image Rendering and Memory Management
5.1 Image Loading Principle
Flutter loads images via ImageProvider , resolves to an ImageStream , decodes to a texture, and finally paints it. When the widget is disposed, the cache is cleared and the texture is released.
5.2 Image Loading Governance
Large image lists cause high memory usage and slow loading. Strategies include pre‑caching, lazy loading, using WebP format, CDN compression, and shared native memory.
5.3 Image Pre‑cache
Use precacheImage at appropriate moments to load images into memory before they appear on screen.
5.4 Image Resource Optimization
Prefer WebP format and CDN‑based size reduction to minimize transfer size.
5.5 Image Memory Optimization
Share native memory between Flutter and the host, use disk cache, and control cacheWidth/cacheHeight to limit resolution and avoid duplicate memory allocation.
Conclusion
The article outlines a comprehensive set of Flutter performance‑optimization practices, including UI‑thread techniques (ViewModel splitting, Provider Selector, isolate off‑loading, widget caching, fine‑grained setState , const ), GPU‑thread tactics (RepaintBoundary, avoid ClipPath and Opacity), long‑list handling, and image loading optimizations, aiming to improve rendering smoothness and user experience.
Ctrip Technology
Official Ctrip Technology account, sharing and discussing growth.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.