javascriptiosreactjsreact-nativeexpo

React Native: Image pop-up (lazy loading) on first run despite using prefetched local images


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;

Solution

  • 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:

    1. Replacing RN's stock ImageBackground component with Image from Expo

    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.

    1. Pre-loading the images by importing them in separate files...

    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`),
      }
    
    1. Cropping the images to fit the sizes of their components

    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.

    1. Updating Expo and switching to React Native's New Architecture

    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.