Mobile Development 20 min read

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.

Ctrip Technology
Ctrip Technology
Ctrip Technology
Flutter Rendering Performance Optimization Practices at Ctrip Train Ticket

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.

fluttermobileperformanceoptimizationrenderingisolateProvider
Ctrip Technology
Written by

Ctrip Technology

Official Ctrip Technology account, sharing and discussing growth.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.