Performance and Optimization page
Improve the application’s launch time by implementing lazy loading.
Overview
In this section, you will:
- Make smaller JavaScript bundles with code splitting.
- Use React’s
lazy
andSuspense
APIs to implement lazy loading. - Learn when it’s best to use dynamic imports.
Objective 1: Lazy load the Map
view
In a previous section, we added a Map
view that has a large dependency: the react-native-maps
package.
Large packages have a negative impact on the application’s startup time, but there’s a solution: lazy loading!
Let’s lazy load the Map
view in our app so it launches faster.
JavaScript bundle size
A “bundle” (file) is created with all of the JavaScript code when the application is built, along with any additional assets (like images). As more code is added to the application, the size of this JavaScript bundle will increase.
The larger bundle can lead to longer startup times for the application because the bundle must be loaded, parsed, and ran before the app can be used. This longer launch time translates into a worse user experience as more code is added. Our app should improve as we add features without sacrificing launch time!
Making smaller JavaScript bundles
Currently, our existing imports are “static” import
declarations that are defined at the top of the file:
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { SafeAreaView } from "react-native"
import Analytics from "./screens/Analytics"
import Home from "./screens/Home"
const AppTabs = createBottomTabNavigator()
const App: React.FC = () => {
return (
<SafeAreaView style={{ height: "100%", width: "100%" }}>
<NavigationContainer>
<AppTabs.Navigator initialRouteName="Home">
<AppTabs.Screen component={Home} name="Home" />
<AppTabs.Screen component={Analytics} name="Analytics" />
</AppTabs.Navigator>
</NavigationContainer>
</SafeAreaView>
)
}
export default App
We can split our code into multiple bundles by using the dynamic import()
syntax from JavaScript.
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { useEffect, useState } from "react"
import { SafeAreaView } from "react-native"
import Home from "./screens/Home"
const Analytics: React.FC = () => {
const [analyticsView, setAnalyticsView] = useState(null)
useEffect(() => {
async function loadView() {
const analyticsModule = await import("./screens/Analytics")
if (analyticsModule.default) {
setAnalyticsView(analyticsModule.default)
}
}
loadView()
}, [])
if (!analyticsView) {
return <Loading />
}
return analyticsView
}
const AppTabs = createBottomTabNavigator()
const App: React.FC = () => {
return (
<SafeAreaView style={{ height: "100%", width: "100%" }}>
<NavigationContainer>
<AppTabs.Navigator initialRouteName="Home">
<AppTabs.Screen component={Home} name="Home" />
<AppTabs.Screen component={Analytics} name="Analytics" />
</AppTabs.Navigator>
</NavigationContainer>
</SafeAreaView>
)
}
export default App
Now with the dynamic import in place, the Analytics
view in the code above will be split into a separate JavaScript bundle.
This means that any of its code (including the code it imports) will be in a separate bundle.
This will keep the main app bundle smaller over time as the Analytics
view grows in size.
Using React’s lazy
and Suspense
APIs
This dynamic import()
code takes a lot of lines to implement this one improvement:
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { useEffect, useState } from "react"
import { SafeAreaView } from "react-native"
import Home from "./screens/Home"
const Analytics: React.FC = () => {
const [analyticsView, setAnalyticsView] = useState(null)
useEffect(() => {
async function loadView() {
const analyticsModule = await import("./screens/Analytics")
if (analyticsModule.default) {
setAnalyticsView(analyticsModule.default)
}
}
loadView()
}, [])
if (!analyticsView) {
return <Loading />
}
return analyticsView
}
const AppTabs = createBottomTabNavigator()
const App: React.FC = () => {
return (
<SafeAreaView style={{ height: "100%", width: "100%" }}>
<NavigationContainer>
<AppTabs.Navigator initialRouteName="Home">
<AppTabs.Screen component={Home} name="Home" />
<AppTabs.Screen component={Analytics} name="Analytics" />
</AppTabs.Navigator>
</NavigationContainer>
</SafeAreaView>
)
}
export default App
If we had to write this for every single component that we wanted to import dynamically, we would have a lot of boilerplate repeated over and over.
Thankfully, React provides two APIs to simplify dynamic imports:
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { Suspense, lazy } from "react"
import { SafeAreaView } from "react-native"
import Loading from "./components/Loading"
import Home from "./screens/Home"
const Analytics = lazy(() => import("./screens/Analytics"))
const AppTabs = createBottomTabNavigator()
const AnalyticsLazyLoaded: React.FC = () => {
return (
<Suspense fallback={<Loading />}>
<Analytics />
</Suspense>
)
}
const App: React.FC = () => {
return (
<SafeAreaView style={{ height: "100%", width: "100%" }}>
<NavigationContainer>
<AppTabs.Navigator initialRouteName="Home">
<AppTabs.Screen component={Home} name="Home" />
<AppTabs.Screen component={AnalyticsLazyLoaded} name="Analytics" />
</AppTabs.Navigator>
</NavigationContainer>
</SafeAreaView>
)
}
export default App
In the code above, we’ve replaced the static import
with a dynamic import()
within lazy
.
When the Analytics tab is tapped on, React Native will load the component
passed to Screen
.
Then, the <Suspense>
component will render <Loading>
while the Analytics module is being imported.
When the module has loaded, the <Analytics>
component will be displayed.
Selectively using dynamic imports
You might wonder if it’s a good idea to use dynamic import()
statements everywhere.
Surprisingly, the answer is no! The benefit of dynamic imports is that the bundle is split up into separate files, but this comes with a small cost.
Each time a bundle is loaded, the JavaScript has to be parsed and ran. This takes a little bit of time for each bundle, so it’s best to only split your bundle in a few key places in your application. This can be in views where there is a large dependency (like our Maps view), or between tabs in the app (like we’ve shown above).
Setup 1
✏️ Update src/screens/RestaurantList/RestaurantList.tsx to be:
import { StackScreenProps } from "@react-navigation/stack"
import { Suspense, lazy, useState } from "react"
import { RestaurantsStackParamList } from "../../App"
import Loading from "../../components/Loading"
import Tabs from "../../components/Tabs"
import Box from "../../design/Box"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurants } from "../../services/pmo/restaurant"
import List from "./components/List"
import Map from "./components/Map"
// Exercise: Change the static Map `import` statement to a dynamic `import()`.
export interface RestaurantListProps
extends StackScreenProps<RestaurantsStackParamList, "RestaurantList"> {}
const RestaurantList: React.FC<RestaurantListProps> = ({ route }) => {
const { state, city } = route.params
const {
data: restaurants,
error,
isPending,
} = useRestaurants({ state: state.short, city: city.name })
const [tab, setTab] = useState<string>("list")
if (error) {
return (
<Screen>
<Box padding="m">
<Typography variant="heading">Error loading restaurants: </Typography>
<Typography variant="body">{error.message}</Typography>
</Box>
</Screen>
)
}
if (isPending) {
return <Loading />
}
return (
<>
<Tabs
options={[
{ label: "List", value: "list" },
{ label: "Map", value: "map" },
]}
onChange={setTab}
value={tab}
/>
<Screen noScroll>
{tab === "list" && restaurants && <List restaurants={restaurants} />}
{/* Exercise: Use `<Suspense>` to load the Map tab on the screen. */}
{tab === "map" && restaurants && <Map restaurants={restaurants} />}
</Screen>
</>
)
}
export default RestaurantList
Verify 1
Watch the output of the npm start
command while it’s running.
When you’ve completed this exercise, you’ll notice that there’s a new bundle loaded when you go to the Map view:
BUILD SUCCESSFUL in 7s
199 actionable tasks: 15 executed, 184 up-to-date
info Connecting to the development server...
info Starting the app on "emulator-5554"...
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.placemyorder/.MainActivity }
BUNDLE ./index.js
LOG Running "PlaceMyOrder" with {"rootTag":11}
BUNDLE src/screens/RestaurantList/components/Map/index.ts
Exercise 1
Inside of RestaurantList.tsx
:
- Change the static Map
import
statement to a dynamicimport()
. - Use
<Suspense>
to load the Map tab on the screen.
Solution 1
If you’ve implemented the solution correctly, you will see the new BUNDLE
line logged while the server is running.
Click to see the solution
✏️ Update src/screens/RestaurantList/RestaurantList.tsx to be:
import { StackScreenProps } from "@react-navigation/stack"
import { Suspense, lazy, useState } from "react"
import { RestaurantsStackParamList } from "../../App"
import Loading from "../../components/Loading"
import Tabs from "../../components/Tabs"
import Box from "../../design/Box"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurants } from "../../services/pmo/restaurant"
import List from "./components/List"
const Map = lazy(() => import("./components/Map"))
export interface RestaurantListProps
extends StackScreenProps<RestaurantsStackParamList, "RestaurantList"> {}
const RestaurantList: React.FC<RestaurantListProps> = ({ route }) => {
const { state, city } = route.params
const {
data: restaurants,
error,
isPending,
} = useRestaurants({ state: state.short, city: city.name })
const [tab, setTab] = useState<string>("list")
if (error) {
return (
<Screen>
<Box padding="m">
<Typography variant="heading">Error loading restaurants: </Typography>
<Typography variant="body">{error.message}</Typography>
</Box>
</Screen>
)
}
if (isPending) {
return <Loading />
}
return (
<>
<Tabs
options={[
{ label: "List", value: "list" },
{ label: "Map", value: "map" },
]}
onChange={setTab}
value={tab}
/>
<Screen noScroll>
{tab === "list" && restaurants && <List restaurants={restaurants} />}
{tab === "map" && restaurants && (
<Suspense fallback={<Loading />}>
<Map restaurants={restaurants} />
</Suspense>
)}
</Screen>
</>
)
}
export default RestaurantList
Next steps
Now we’ve got a complete and performant application. Let’s finish out our work by learning about Building React Native Apps.