I'm stuck on a bug in my RN Expo app and I cannot fathom what I'm doing wrong.
I've been trying to fix the bug that makes all of my images pop-up about half a second after rendering my pages when the user enters them for the first time after booting/reloading the app.
At first, I thought that the reason for it might be that the images I'm using are too big so I've temporarily, for debugging purposes, replaced them with small webps that I have already used in another project without any problems. This turned out to not be the case.
Then, I've tried pre-fetching my images using the code provided in Expo docs. This changed nothing, whatsoever.
I've also, after seeing such suggestion in another thread, made a development build to see if it would fix it - it did not.
Here is the link to my repo on GitHub: https://github.com/irolinski/Well_CBT
Here's a GIF of how the bug looks like: https://gifyu.com/image/SyL5p
Here are the two components that I was focusing on: the second one is just a random one that I've added images to.
_layout.tsx
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect, useState } from "react";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Provider as StateProvider } from "react-redux";
import { store } from "@/state/store";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { Asset } from "expo-asset";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
function cacheImages(images: any[]) {
return images.map((image) => {
if (typeof image === "string") {
return Image.prefetch(image);
} else {
return Asset.fromModule(image).downloadAsync();
}
});
}
export default function RootLayout() {
const [fontsLoaded] = useFonts({
Inter: require("../assets/fonts/Inter-Standard.ttf"),
InterItalic: require("../assets/fonts/Inter-Italic.ttf"),
KodchasanRegular: require("../assets/fonts/Kodchasan-Regular.ttf"),
KodchasanMedium: require("../assets/fonts/Kodchasan-Medium.ttf"),
});
const [imagesLoaded, setImagesLoaded] = useState(false);
// Load any resources or data that you need before rendering the app
useEffect(() => {
async function loadResourcesAndDataAsync() {
try {
SplashScreen.preventAutoHideAsync();
const imageAssets = cacheImages([
require("../assets/images/affirmation-images/California-backyard-1.webp"),
require("../assets/images/affirmation-images/California-backyard-2.webp"),
require("../assets/images/affirmation-images/California-backyard-3.webp"),
require("../assets/images/affirmation-images/California-backyard-4.webp"),
require("../assets/images/affirmation-images/english-countryside-1.webp"),
require("../assets/images/affirmation-images/english-countryside-2.webp"),
require("../assets/images/affirmation-images/english-countryside-3.webp"),
require("../assets/images/affirmation-images/english-countryside-4.webp"),
]);
await Promise.all([...imageAssets]);
console.log("images cached");
} catch (err) {
console.warn(err);
} finally {
setImagesLoaded(true);
SplashScreen.hideAsync();
}
}
loadResourcesAndDataAsync();
}, []);
if (!imagesLoaded || !fontsLoaded) {
return null;
}
return (
<StateProvider store={store}>
<GestureHandlerRootView>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="tools/classic_cbt/cda"
options={{ headerShown: false }}
/>
<Stack.Screen
name="tools/classic_cbt/journal"
options={{ headerShown: false }}
/>
<Stack.Screen
name="tools/relax/breathing"
options={{ headerShown: false }}
/>
<Stack.Screen
name="tools/distract/phone"
options={{ headerShown: false }}
/>
<Stack.Screen name="+not-found" />
</Stack>
</GestureHandlerRootView>
</StateProvider>
);
}
app/(tabs)/Tools.tsx
import React from "react";
import { View } from "react-native";
import FrameMenu from "@/components/FrameMenu";
import Text from "@/components/global/Text";
import ToolCard from "../../components/ToolCard";
const Tools = () => {
return (
<FrameMenu title="Tools">
<View>
<Text className="mb-8 mt-2 text-left text-2xl">Classic</Text>
<ToolCard
name="Identify the Distortions"
image={require("@/assets/images/affirmation-images/english-countryside-1.webp")}
link={"/tools/classic_cbt/cda"}
/>
<ToolCard
name="Mood Journal"
image={require("@/assets/images/affirmation-images/english-countryside-2.webp")}
link={"/tools/classic_cbt/journal"}
/>
<ToolCard
name="Ground Yourself"
image={require("@/assets/images/affirmation-images/english-countryside-3.webp")}
link={"tools/classic_cbt/grounding"}
></ToolCard>
<Text className="mb-8 mt-2 text-left text-2xl">Relax</Text>
<ToolCard
name="Breathing excercises"
link={"/tools/relax/breathing"}
image={require("@/assets/images/affirmation-images/english-countryside-4.webp")}
/>
<ToolCard
name="Muscle relaxation"
link={"/tools/relax/muscleRelaxation"}
image={require("@/assets/images/affirmation-images/english-countryside-1.webp")}
/>
<Text className="mb-8 mt-2 text-left text-2xl">Distract yourself</Text>
<ToolCard
name="Phone to a friend"
image={require("@/assets/images/affirmation-images/english-countryside-1.webp")}
link={"/tools/distract/phone"}
/>
<ToolCard
name="Listen to music"
link={"/tools/distract/music"}
image={require("@/assets/images/affirmation-images/english-countryside-1.webp")}
/>
</View>
</FrameMenu>
);
};
export default Tools;
By now, I've managed to go around this bug by doing a couple of things. Either some of them or all of the below could have helped.
Disclaimer: this does not resolve the root cause of the buggy pre-fetch feature.
What helped with the lazy image load was:
Image placed inside a View with style={{position: "absolute", width: "100%", height: "100%}}.
This made all the images load faster in some cases eliminating the pop-up in some cases (pages with little image content). Expo Image also offers some onLoad animations that make the pop-up much less grating and noticeable even if it happens.
There are two ways I've done this with success:
If I'm correct, this should force React Native to load the images during runtime just as it would do with any other import.
Solution 1
Exporting images either as components or inside an array/object.
Example:
images.ts import image_1 from "@/assets/images/tools/image_1.webp";
export const imagesForComponent = [
image_1
];
Component.tsx
import { imagesForComponent } from '@/assets/images/tools/images.ts';
import { Image } from 'expo-image';
<View>
<Image
style={[
location === "phone" && styles.imagePhone,
location === "about" && styles.imageAbout,
]}
source={
!phoneState ? phoneFacePlaceholder : phoneFaces[faceNumber]
}
/>
</View>
Solution 2
Setting value of an exported object to a required image also seems to work fine.
Example:
objects.ts
export const object = {
title: "Title",
subtitle: "Subtitle",
bgImage: require(`@/assets/images/image_1.webp`),
}
It seems to me that a part of the slowdown effect that caused the pop-up was a side-effect of React Native trying to calculate how to crop an image to fit the component if the image was noticeably larger.
After reading posts of other users with the same problem I've concluded that the lazy load issues were often reported by people using Expo SDK 51. I've updated to SDK 52 as soon as it came out, and with it I've also migrated to Raect Native's New Architecture (which is almost obligatory with SDK 52). This made everything run much more smoothly.