- Published on
Athoni: Mobile Optimization - How I Improved App Performance from 20-30 FPS to 50-60 FPS?
- Authors
- Name
- Jack Nguyen
At Athoni, a game-based learning platform, we faced a significant performance challenge. Our app was running at a sluggish 20-30 frames per second (FPS), far below the smooth experience we wanted to deliver to our users. After diving into the issue, we identified a major culprit: an excessive number of nested React Context Providers. Here's how we tackled this problem and boosted performance by 50%.

Table of Contents
The Problem: Context Providers and Re-Rendering

In our app, we had a large number of Providers, as shown in the image below. While React Context is a powerful tool for state management, it can become a bottleneck when overused.
When we called setState
on any Provider, all the child components that fetch state from the context were triggered to re-render. Combining this with heavy assets like images, animations, and Lottie files, the re-renders became overwhelming, causing the FPS to plummet.
Code example:
LanguageProvider.tsx
const LanguageProvider: React.FC = ({ children }) => {
const [language, setLanguage] = useState('en')
const value = useMemo(
() => ({
language,
setLanguage,
}),
[language]
)
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>
}
App.tsx
const App: React.FC = () => {
return (
<LanguageProvider>
<Home />
</LanguageProvider>
)
}
Home.tsx
const Home: React.FC = () => {
const { language } = useContext(LanguageContext)
return (
<div>
<h1>{language}</h1>
<Collection />
<img src="heavy-image.png" alt="Heavy Image" /> // Heavy asset
</div>
)
}
Collection.tsx
const Collection: React.FC = () => {
const { language } = useContext(LanguageContext)
const { collection } = useContext(CollectionContext)
return (
<div>
<h2>{language}</h2>
{collection.map((item) => (
<Item key={item.id} item={item} />
))}
<LottieView source={require('animation.json')} /> // Heavy asset
</div>
)
}
Example Flow of a Re-Render:
LanguageProvider
updates the language state.- This triggers a re-render in
Home
andCollection
. - The heavy image and Lottie animation in
Home
andCollection
are re-rendered.
This process led to a severe drop in performance, especially on devices with limited processing power.
The Solution: Optimizing Context and Rendering
To resolve this, I adopted the following strategies:
useMemo
and React.memo
1. Why: These tools help avoid unnecessary re-renders by memoizing values and components.
How: I wrapped components with
React.memo
to ensure they only re-render when their props change. Additionally, I useduseMemo
to memorize expensive computations or derived state.Example:
const OptimizedComponent = React.memo(({ value }: { value: string }) => { console.log('Rendered with value:', value) return <div>{value}</div> }) const ParentComponent = () => { const [count, setCount] = useState(0) const memoizedValue = useMemo(() => `Count: ${count}`, [count]) return ( <div> <button onClick={() => setCount((prev) => prev + 1)}>Increment</button> <OptimizedComponent value={memoizedValue} /> </div> ) }
In this setup,
OptimizedComponent
only re-renders whenmemoizedValue
changes.
Now, we will apply this technique (useMemo) to the previous example:
Home.tsx
const HeavyImage = React.memo(() => <img src="heavy-image.png" alt="Heavy Image" />)
const Home: React.FC = () => {
const { language } = useContext(LanguageContext)
return (
<div>
<h1>{language}</h1>
<Collection />
<HeavyImage />
</div>
)
}
Collection.tsx
const HeavyAnimation = React.memo(() => <LottieView source={require('animation.json')} />)
const Collection: React.FC = () => {
const { language } = useContext(LanguageContext)
const { collection } = useContext(CollectionContext)
const CollectionItems = useMemo(
() => collection.map((item) => <Item key={item.id} item={item} />),
[collection]
) // Memorize the collection items - Alternative Way (like React.memo)
return (
<div>
<h2>{language}</h2>
{CollectionItems}
<HeavyAnimation />
</div>
)
}
useReducer
for State Management
2. Why: Managing multiple state updates with
useState
can cause excessive re-renders.How: I replaced multiple
useState
calls with a singleuseReducer
to batch updates and reduce rendering overhead.Example:
const initialState = { count: 0, loading: false } const reducer = (state, action) => { switch (action.type) { case 'increment': return { ...state, count: state.count + 1 } case 'setLoading': return { ...state, loading: action.payload } default: return state } } const Counter = () => { const [state, dispatch] = useReducer(reducer, initialState) return ( <div> <button onClick={() => dispatch({ type: 'increment' })}>Increment</button> <button onClick={() => dispatch({ type: 'setLoading', payload: true })}>Set Loading</button> </div> ) }
react-native-fast-image
for Image Caching
3. Why: Rendering images from the network repeatedly adds to the app's workload.
How: I used the
FastImage
library to cache images and improve loading times.Example:
import FastImage from 'react-native-fast-image' const ImageComponent = () => ( <FastImage style={{ width: 100, height: 100 }} source={{ uri: 'https://example.com/image.png' }} resizeMode={FastImage.resizeMode.contain} /> )
useNativeDriver
for Animations
4. - Why: Animations running on the JavaScript thread can cause jank.
- How: I enabled the
useNativeDriver
option inAnimated.timing
to offload animations to the native driver, improving performance. - Example:
Animated.timing(animatedValue, { toValue: 1, duration: 500, useNativeDriver: true, // Offload to native driver }).start()
5. Reducing Nested Providers
- Why: Excessive nesting of Providers increases the complexity of the app.
- How: I consolidated state management logic by grouping related contexts or using a global state management library when appropriate.
The Results
By implementing these optimizations, I successfully boosted our app's performance from 20-30 FPS to 50-60 FPS. This improvement was not only measurable but also highly noticeable to our users, resulting in smoother animations, faster load times, and an overall better experience.
Conclusion
While React Context is a fantastic tool, its overuse can lead to significant performance issues. Through careful analysis and targeted optimizations, I demonstrated that even complex issues can be resolved. If you're facing similar challenges, consider these strategies to enhance your app's performance and delight your users!