Mobile Development 16 min read

How We Doubled React Native First‑Screen Speed in Tencent’s Penguin Tutoring App

This article details the migration of Tencent's Penguin Tutoring app from the Plato framework to React Native, outlines first‑screen performance bottlenecks, presents caching and rendering optimizations, redesigns the carousel component to eliminate flicker, and shares a collection of RN pitfalls and their solutions.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
How We Doubled React Native First‑Screen Speed in Tencent’s Penguin Tutoring App

Introduction

This article takes about 8 minutes to read and covers four main topics: the practice of React Native in Tencent's Penguin Tutoring, first‑screen performance optimization, carousel and animation solutions, and a list of RN pitfalls.

Background

As business requirements grew, the original Plato (a RN‑like framework) could no longer meet the needs, so the team began a risky migration from Plato to RN. In the Penguin Tutoring app, seven pages are written by the front‑end team, with the Home and List pages being the most critical, both built with Plato.

The migration focuses on refactoring and optimizing the Home and List pages and upgrading RN to version 0.58, which introduces compatibility issues for other pages.

Home Page Module Analysis

The Home page consists of five sections: top function area, subject list, Banner, newcomer info, and course card list.

The ViewPager component uses ScrollView on iOS and AndroidViewPager on Android. The rendering logic is shown in the following diagram:

Optimization Path

After establishing the overall architecture, development became clearer and faster, but the first‑generation product revealed many issues.

All performance tests were conducted on a Vivo X9 (4 GB RAM) released at the end of 2016, because 75 % of users are Android.

Although caching was implemented, the loading time remained long. The following table shows the time spent in each stage of the first‑screen process (Android registration time is excluded because it cannot be controlled by the front‑end):

Stage                Duration
JS business code    400 ms
AsyncStorage cache   300 ms
React render        730 ms
Render to screen    820 ms

AsyncStorage’s persistence is based on a JS‑Bridge communication with the native layer, which is far slower than Web LocalStorage. To obtain cached data faster, the team executed a small JS registration snippet during RN module registration:

AppRegistry.registerComponent('rn', () => DiscoverCourse); // iOS simulator
AppRegistry.registerComponent('order', () => Orders); // My‑Orders
AppRegistry.registerComponent('message', () => Messages); // Messages
AppRegistry.registerComponent('coursebreak', () => CourseBreak); // Break
AppRegistry.registerComponent('coursebreakall', () => Articles); // More breaks
AppRegistry.registerComponent('index', () => DiscoverCourse); // Discover page
AppRegistry.registerComponent('packdetail', () => CoursePackDetail); // List page

By fetching data at this early stage, the app’s second launch became noticeably faster—more than twice the speed—by reducing JS‑Bridge communication time.

To further reduce JS‑Bridge time, data can be stored under a single key in AsyncStorage instead of many separate keys.

First‑Screen Rendering

Even after caching, the first‑screen speed still lagged. Two problems were identified:

How to handle new users without cached data?

White‑screen issue in the Banner area.

The solution was to render only the visible parts initially: grade selection list, Banner, newcomer area, and the first three course cards. The remaining content is rendered in a second pass.

Only the visible parts need to be cached; the Banner needs only one image cached.

A dedicated data‑processing module was created to handle first‑screen data uniformly.

React 16’s Fragment component was used to reduce unnecessary component nesting.

Banner Optimization

Special thanks to @charryhuang for his dedication to solving the Banner problem.

The Banner is an infinite carousel with several issues:

Slow upload on Android Image component.

When sliding continuously, the carousel stops at the boundary until the animation ends.

Selected item size is 100 % while side items are 94 %; on Android this causes flicker during repositioning.

After researching open‑source carousel components, the team rewrote a custom carousel based on react‑native‑snap‑carousel. The core idea is to add extra items on both sides (e.g., 45[12345]12) so that when the offset exceeds a threshold, the carousel instantly repositions without visible jumps.

On Android the original FlatList caused jitter because of inertia. Replacing it with ViewPagerAndroid, which allows only one page per swipe and has no inertia, eliminated the jitter. The offset listener now triggers repositioning when the offset exceeds left or right thresholds, ensuring smooth transitions.

For the scaling animation, Animated.event and interpolate were used to map scroll offset to item scale. The same input range is shared among items with identical indices to keep their sizes synchronized during repositioning.

onScroll={Animated.event([ { nativeEvent: { x: this.scrollX } } ], { listener: this.handleScroll })}

this.scrollX.interpolate({
  inputRange: [0, 1],
  outputRange: [inactiveScale, 1]
});

When the offset is within a certain range, the item’s scale transitions smoothly between 0.94 and 1, avoiding size mismatches after repositioning.

The final carousel works without flicker on both iOS and Android.

Other Optimizations

General RN performance tips were also applied, as shown in the following diagram:

Pitfalls Summary

Image Component Border Issue

On Android 5.0 and below, setting borderWidth or borderRadius on an Image makes the image turn black and crashes the app after a few seconds.

Solution: wrap the Image in a View, set overflow: 'hidden' and borderRadius on the View instead.

Android measure Returns Empty Values

Calling measure on a View sometimes returns empty pageX and pageY. Adding an onLayout prop (even an empty function) to the View resolves the issue.

ViewPagerAndroid White‑Screen Issue

If ViewPagerAndroid is not unmounted between renders, a white screen appears even though content exists.

Solution: assign a different key to the ViewPagerAndroid on each render so that React forces a full re‑render.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Mobile DevelopmentPerformance OptimizationAndroidReact NativeCarousel
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

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.