WellAlly Logo
WellAlly康心伴
Development

How We Reduced Our React Native App's Load Time by 50%: A Fitness App Performance Case Study

A practical breakdown of how we cut our app's time-to-interactive from 4.2s to 2.1s using bundle analysis, lazy loading, image optimization, and migrating to Zustand.

W
2025-12-12
8 min read

In the competitive world of mobile fitness apps, every second counts. Our app, "FitLife," was feature-rich, but we had a growing problem: user analytics revealed a 35% drop-off rate during the initial loading screen. A sluggish start is a silent killer for user engagement. Our time-to-interactive (TTI) was a painful 4.2 seconds on an average Android device. This case study breaks down the exact steps we took to slash that load time by 50%.

We'll walk through a practical, four-step optimization playbook that you can apply to your own React Native projects. We'll cover identifying bottlenecks, deferring non-critical code, shrinking our assets, and choosing a more performant state management library.

Prerequisites:

  • Familiarity with React Native and JavaScript.
  • Node.js and a React Native development environment set up.
  • Tools: react-native-bundle-visualizer, react-native-fast-image.

This matters because a faster app isn't just a "nice-to-have"; it directly impacts user satisfaction, retention, and ultimately, your app's success.

Understanding the Problem

Before diving in, we needed to understand why our app was so slow. Profiling revealed several culprits:

  • A Bloated JavaScript Bundle: At 3.5 MB, our JS bundle was massive. It contained libraries and components that weren't needed for the initial screen.
  • Eager Loading of All Screens: Our navigation stack was importing all screen components upfront, even if the user never navigated to them.
  • Unoptimized Images: High-resolution workout images and user avatars were being loaded on startup, blocking the main thread.
  • State Management Overhead: Our Redux setup, while powerful, involved significant boilerplate and a large initial state object that slowed down hydration.

Our approach was simple: measure, optimize, and measure again. We targeted the lowest-hanging fruit first to achieve the biggest wins quickly.

Prerequisites

To follow along, you'll need a few tools to analyze and optimize your app.

  • Bundle Analysis Tool: We'll use react-native-bundle-visualizer to inspect our JS bundle.

    code
    # Install the visualizer
    npm install --save-dev react-native-bundle-visualizer
    
    # Generate bundle stats
    npx react-native-bundle-visualizer
    
    Code collapsed

    This command generates an interactive treemap of your bundle, showing exactly which packages are taking up the most space.

  • Image Optimization Library: We'll use react-native-fast-image for better caching and performance.

    code
    npm install react-native-fast-image
    cd ios && pod install
    
    Code collapsed

Step 1: Analyzing and Trimming the JS Bundle

Our first step was to use the bundle visualizer. The output was revealing. A large portion of our bundle was consumed by a charting library for workout history and a complex animation library for workout completion—neither of which was needed on the home screen.

What we're doing

We're identifying the largest dependencies in our JavaScript bundle to see what can be removed or loaded later.

Implementation

After running react-native-bundle-visualizer, we got an HTML report.

(Image suggestion: A screenshot of a bundle analyzer treemap, highlighting large, non-essential libraries like moment.js or a heavy charting library).

The visualizer showed that victory-native (our charting library) and lottie-react-native were contributing over 600KB to the bundle.

How it works

The bundle visualizer parses the Metro bundler's output and creates a visual map. This allows you to quickly spot "dead code" or libraries that are unnecessarily large. We discovered we were importing the entire lodash library instead of specific functions, a common pitfall.

Common pitfalls

  • Importing entire libraries: Instead of import { debounce } from 'lodash';, use import debounce from 'lodash/debounce'; to avoid bundling the whole library.
  • Ignoring dev dependencies: Ensure that libraries meant only for development (like storybook) are not making their way into your production bundle.

Result: By removing unused dependencies and cherry-picking imports, we trimmed 500KB from our bundle size. Load Time Improvement: ~0.5s.

Step 2: Implementing Lazy Loading for Screens and Components

Why load the "WorkoutDetail" screen before the user even taps on a workout? Lazy loading defers loading code for a component until it's actually needed.

What we're doing

We'll use React.lazy and Suspense to split our code and load screens on demand. This is one of the most effective ways to reduce initial load time.

Implementation

We changed our navigation stack from static imports to dynamic ones.

Before:

code
// src/navigation/AppNavigator.js
import HomeScreen from '../screens/HomeScreen';
import WorkoutDetailScreen from '../screens/WorkoutDetailScreen';
import ProfileScreen from '../screens/ProfileScreen';

// ... Stack.Screen definitions for all screens
Code collapsed

After:

code
// src/navigation/AppNavigator.js
import React, { Suspense } from 'react';
import { View, ActivityIndicator } from 'react-native';
import HomeScreen from '../screens/HomeScreen';

// Lazily load screens that are not the initial route
const WorkoutDetailScreen = React.lazy(() => import('../screens/WorkoutDetailScreen'));
const ProfileScreen = React.lazy(() => import('../screens/ProfileScreen'));

const AppNavigator = () => {
  return (
    <Suspense fallback={<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}><ActivityIndicator /></View>}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="WorkoutDetail" component={WorkoutDetailScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
      </Stack.Navigator>
    </Suspense>
  );
};
Code collapsed

How it works

React.lazy lets you render a dynamic import as a regular component. When React tries to render WorkoutDetailScreen, it triggers the dynamic import(). While the code chunk is being fetched, the Suspense component's fallback UI is shown to the user.

Common pitfalls

  • No fallback UI: Forgetting to wrap lazy components in <Suspense> will cause your app to crash.
  • Lazy loading everything: Don't lazy load your initial screen or critical components, as this can create a UI lag.

Result: Code splitting reduced our initial JS bundle size by another 1.2MB. Load Time Improvement: ~1.0s.

Step 3: Optimizing Image Delivery

Our app's home screen featured a carousel of workout plans, each with a high-resolution banner image. These images were a major performance bottleneck.

What we're doing

We are replacing the default React Native <Image> component with react-native-fast-image for better caching and using modern, compressed image formats like WebP.

Implementation

code
// src/components/WorkoutCard.js
import React from 'react';
import FastImage from 'react-native-fast-image';

const WorkoutCard = ({ workout }) => {
  // Use a .webp URL from a CDN if possible
  const imageUrl = `${workout.imageUrl}?format=webp&quality=80`;

  return (
    <FastImage
      style={{ width: 200, height: 150 }}
      source={{
        uri: imageUrl,
        priority: FastImage.priority.normal,
        cache: FastImage.cacheControl.immutable, // Aggressively cache images
      }}
      resizeMode={FastImage.resizeMode.cover}
    />
  );
};

export default WorkoutCard;
Code collapsed

How it works

react-native-fast-image uses native libraries (SDWebImage on iOS, Glide on Android) for image loading and caching, which is significantly more performant than React Native's default implementation. The cache: FastImage.cacheControl.immutable property tells the app to cache the image aggressively, making subsequent loads instant. Using WebP format reduces image file sizes by 25-35% compared to JPEG without significant quality loss.

Common pitfalls

  • Not resizing images: Serving a 1080p image for a 200px thumbnail wastes bandwidth and memory. Use a CDN or backend service to resize images on the fly.
  • Ignoring local assets: Use tools like TinyPNG or Squoosh to compress images bundled with your app.

Result: Optimized images improved perceived performance and reduced network payload. Load Time Improvement: ~0.4s.

Step 4: Migrating State Management from Redux to Zustand

Our final frontier was state management. Redux Toolkit served us well, but its boilerplate and the size of the redux-persist hydrated state were contributing to startup delays. For our app's complexity, it was overkill. We decided to migrate to Zustand.

What we're doing

Replacing Redux Toolkit with Zustand to reduce boilerplate, bundle size, and initial state hydration time.

Implementation

Before (Redux Toolkit Slice):

code
// src/store/userSlice.js
import { createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: { profile: null, settings: {} },
  reducers: {
    setUser: (state, action) => {
      state.profile = action.payload;
    },
  },
});
// ... plus store configuration, providers, etc.
Code collapsed

After (Zustand Store):

code
// src/store/userStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useUserStore = create(
  persist(
    (set) => ({
      profile: null,
      settings: {},
      setUser: (userProfile) => set({ profile: userProfile }),
    }),
    {
      name: 'user-storage', // key in AsyncStorage
    }
  )
);
Code collapsed

How it works

Zustand is a small, fast, and scalable state-management solution built on hooks. It has minimal boilerplate and is generally more lightweight than Redux. Its performance is often better because component updates are more granular; components re-render only when the exact piece of state they subscribe to changes.

Common pitfalls

  • Putting everything in one store: While tempting, it's better to create separate stores (slices) for different domains of your app (e.g., useUserStore, useWorkoutStore).
  • Not using selectors: To prevent unnecessary re-renders, always select the minimal state needed in your components: const profile = useUserStore(state => state.profile);.

Result: Zustand reduced our state management library footprint and sped up state rehydration. Load Time Improvement: ~0.2s.

Putting It All Together

Here's a summary of our optimizations and their impact on our app's performance.

MetricBefore OptimizationAfter OptimizationImprovement
Time-to-Interactive4.2 seconds2.1 seconds-50%
JS Bundle Size3.5 MB1.8 MB-48%
User Drop-off Rate35%12%-65%

The cumulative effect of these changes transformed the user experience. The app felt snappy and responsive from the very first launch.

Conclusion

Optimizing a React Native app's performance is not a one-time task but a continuous process of measurement and refinement. By systematically tackling our largest bottlenecks—bundle size, asset loading, and state management—we were able to achieve a dramatic 50% reduction in load time.

Our key takeaways:

  1. Profile First: Don't guess. Use tools like react-native-bundle-visualizer to find your real performance issues.
  2. Defer Everything Possible: If the user doesn't need it on the first screen, load it later with React.lazy.
  3. Images are Deceivingly Heavy: A proper image optimization strategy is non-negotiable for a media-rich app.
  4. Choose the Right Tool for the Job: A lighter state management library like Zustand can offer significant performance wins for small to medium-sized apps.

We encourage you to take these techniques and apply them to your own projects. Your users will thank you for it.

Resources

#

Article Tags

reactnative
performance
mobile
casestudy
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare Technology
Software Development
User Experience
AI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey