Improving Application Performance with Preloading in React and React Native

In today’s fast-paced digital landscape, an application's success hinges on its performance and load times. Users expect smooth, responsive interfaces, and even minor delays can lead to frustration or disengagement. React and React Native provide robust tools for optimizing performance, such as lazy loading components with Suspense. However, even lazy loading has its drawbacks—particularly the noticeable "white screen" or fallback UI users encounter while components load.

Imagine your app has two modules: Auth and Dashboard. Both modules are lazy-loaded components. After a user registers and navigates to the dashboard, they might face a delay during which the dashboard component is still being downloaded. This can disrupt the user experience.

So how can we avoid this delay and ensure the dashboard loads instantly when users navigate to it? The answer lies in preloading strategies.

What Is Preloading?

Preloading involves downloading components or assets in the background before they are required. By leveraging preloading, we can ensure that components are ready to render immediately when needed. This article demonstrates how to implement a reusable preloading mechanism in React and React Native.


Reusable Preloading Logic

To simplify preloading and lazy loading, let’s create two utilities:

  1. LazyLoader: A wrapper to handle lazy-loaded components with a fallback UI.
  2. lazyWithPreload: A utility to lazy-load components and provide a preload method.

LazyLoader.tsx

This utility wraps a lazy-loaded component with Suspense, providing a smooth fallback UI.

import React, { Suspense, ComponentType } from 'react'
import { View, ActivityIndicator, StyleSheet } from 'react-native'

/**
 * A wrapper to handle lazy-loaded components with a fallback.
 *
 * @param {React.ComponentType} component - The lazy-loaded component to render.
 * @param {React.ReactNode} fallback - Optional fallback component.
 */
export const LazyLoader = ({
  component,
  fallback,
}: {
  component: ComponentType<any>
  fallback?: React.ReactNode
}) => {
  const Component = component
  return (
    <Suspense
      fallback={
        fallback || (
          <View style={styles.container}>
            <ActivityIndicator size="large" color="#999" />
          </View>
        )
      }
    >
      <Component />
    </Suspense>
  )
}

const styles = StyleSheet.create({
  container: { alignItems: 'center', flex: 1, justifyContent: 'center' },
})

lazyWithPreload Function

This utility extends React's lazy function, adding a preload method to download the component in advance.

import React, { lazy } from 'react'

/**
 * Extended LazyExoticComponent type with a preload method.
 */
type LazyWithPreload<T extends React.ComponentType<any>> =
  React.LazyExoticComponent<T> & {
    preload: () => Promise<{ default: T }>
  }

/**
 * A utility for lazy-loading and preloading React components.
 *
 * @param factory - The dynamic import function for the component.
 * @returns The lazy component with an added preload method.
 */
export const lazyWithPreload = <T extends React.ComponentType<any>>(
  factory: () => Promise<{ default: T }>,
): LazyWithPreload<T> => {
  const Component = lazy(factory) as LazyWithPreload<T>
  Component.preload = factory // Attach the preload method
  return Component
}

Usage Examples

React Example

Dashboard.js

import React from 'react'

const Dashboard = () => {
  return <div>Welcome to the Dashboard!</div>
}

export default Dashboard

Main.js

import React, { useEffect } from 'react'
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom'
import { lazyWithPreload } from './lazyWithPreload'
import { LazyLoader } from './LazyLoader'

// Lazy load the Dashboard component with preloading capability
const Dashboard = lazyWithPreload(() => import('./Dashboard'))

const App = () => {
  useEffect(() => {
    // Preload the Dashboard component when the app starts
    Dashboard.preload()
  }, [])

  return (
    <Router>
      <nav>
        <Link to="/dashboard">Go to Dashboard</Link>
      </nav>
      <Switch>
        <Route
          path="/dashboard"
          render={() => <LazyLoader component={Dashboard} />}
        />
      </Switch>
    </Router>
  )
}

export default App

React Native Example

Dashboard.tsx

import React from 'react'
import { View, Text, StyleSheet } from 'react-native'

const Dashboard = () => {
  return (
    <View style={styles.container}>
      <Text>Welcome to the Dashboard!</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
})

export default Dashboard

App.tsx

import React, { useEffect } from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import { lazyWithPreload } from './lazyWithPreload'
import { LazyLoader } from './LazyLoader'

// Lazy load the Dashboard component with preloading capability
const Dashboard = lazyWithPreload(() => import('./Dashboard'))

const Stack = createStackNavigator()

const App = () => {
  useEffect(() => {
    // Preload the Dashboard component when the app starts
    Dashboard.preload()
  }, [])

  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Dashboard">
          {() => <LazyLoader component={Dashboard} />}
        </Stack.Screen>
      </Stack.Navigator>
    </NavigationContainer>
  )
}

export default App

Benefits of Preloading

  • Instant Navigation: Eliminates the wait time when users navigate to a new page or screen.
  • Improved User Experience: Reduces visual disruptions like fallback UIs.
  • Proactive Loading: Ensures critical components are ready when needed.

By integrating lazy loading with a preloading strategy, your React and React Native applications can deliver a smoother, faster experience that users will appreciate.