You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native从Amazon S3加载图片过慢排查求助

React Native 从S3分页加载图片过慢问题

问题概述

更新:已编写最简测试组件SetsTest.js,该组件可快速从Amazon S3获取并渲染图片,说明问题不在AWS/Amplify配置,而是主代码的图片获取、URL构建或渲染逻辑存在问题。

正在开发一款React Native移动端应用,其中一个页面需要实现:进入后从Amazon S3分页加载图片,每次加载21张,以2列布局展示。当前遇到的核心问题是图片渲染极慢(耗时超过10分钟),但所有图片大小仅在20-600KB之间,桶内约有150张图片,调整分页大小后问题仍未改善。确认S3桶权限与Amplify配置正确(可通过浏览器直接访问图片URL),但无法100%确定。

相关代码

SetsTest.js

import React, { useEffect, useState } from "react";
import { View, Image, StyleSheet, Dimensions } from "react-native";
import { Storage } from "aws-amplify";

export default function Sets_Dev() {
  const [imageUrls, setImageUrls] = useState([]);

  useEffect(() => {
    const fetchFirstTwoImages = async () => {
      try {
        const response = await Storage.list("pkmn_set_logos_eng/", {
          maxKeys: 3, // 取3个是因为第一个可能是目录本身
        });

        console.log("S3 Response:", response);

        if (!response.results || response.results.length === 0) {
          console.error("No items returned from S3.");
          return;
        }

        // 过滤掉目录键,然后映射获取URL
        const validResults = response.results.filter(
          (item) => item.key !== "pkmn_set_logos_eng/"
        );

        const urls = await Promise.all(
          validResults.map((item) => Storage.get(item.key, { level: "public" }))
        );

        setImageUrls(urls);
      } catch (error) {
        console.error("Failed to fetch images from S3:", error);
      }
    };

    fetchFirstTwoImages();
  }, []);

  return (
    <View style={styles.container}>
      {imageUrls.map((url, index) => (
        <Image
          key={index}
          source={{ uri: url }}
          style={styles.image}
          resizeMode="contain"
        />
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    flexDirection: "row",
    padding: 10,
  },
  image: {
    width: Dimensions.get("window").width * 0.45,
    height: Dimensions.get("window").width * 0.45,
    margin: 5,
  },
});

Sets.js

import React, { useEffect, useState } from "react";
import colors from "./colors";
import {
  FlatList,
  View,
  Text,
  Dimensions,
  StyleSheet,
  ActivityIndicator,
  TouchableOpacity,
  TextInput,
  Image,
} from "react-native";
import { Storage } from "aws-amplify";

const { width } = Dimensions.get("window");
const PAGE_SIZE = 21; // 更新了分页大小
const CONTAINER_PADDING = 20;
const CONTAINER_WIDTH = (width - 40 - CONTAINER_PADDING * 2) / 2;

export default function Sets({ navigation }) {
  const [images, setImages] = useState([]);
  const [nextToken, setNextToken] = useState(undefined);
  const [hasNextPage, setHasNextPage] = useState(true);
  const [isLoading, setIsLoading] = useState(false);
  const [searchText, setSearchText] = useState("");

  const fetchImages = async () => {
    if (!hasNextPage || isLoading) return;
    setIsLoading(true);
    try {
      const response = await Storage.list("pkmn_set_logos_eng/", {
        pageSize: PAGE_SIZE,
        nextToken: nextToken,
      });

      if (response && response.results && Array.isArray(response.results)) {
        const filteredImages = response.results.filter(
          (item) => item.key !== "pkmn_set_logos_eng/" && !item.key.endsWith("/")
        );

        const imageURLs = await Promise.all(
          filteredImages.map(async (item) => {
            const setId = item.key.split("/")[1].split(".")[0];
            const imageUrl = await Storage.get(item.key, { level: "public" });
            return { setId, imageUrl };
          })
        );

        const setTotals = await Promise.all(
          imageURLs.map((image) => fetchSetTotal(image.setId))
        );

        imageURLs.forEach((image, index) => {
          image.setTotal = setTotals[index];
        });

        setImages((prevImages) => [...prevImages, ...imageURLs]);
        setIsLoading(false);
        setNextToken(response.nextToken);
        setHasNextPage(!!response.nextToken);
      } else {
        throw new Error("Unexpected response format from Storage.list");
      }
    } catch (error) {
      setIsLoading(false);
      setHasNextPage(false);
    }
  };

  const fetchSetTotal = async (setId) => {
    try {
      const response = await fetch(
        `http://MyIp:3000/get-set-total/${setId}`
      );
      if (!response.ok) return null;
      return await response.json();
    } catch (error) {
      return null;
    }
  };

  useEffect(() => {
    fetchImages();
  }, []);

  const renderImageItem = ({ item }) => (
    <TouchableOpacity
      style={styles.imageContainer}
      onPress={() => {
        navigation.navigate("SetClickScreen", {
          setId: item.setId,
          imageUrl: item.imageUrl,
          setTotal: item.setTotal,
        });
      }}
    >
      <View style={styles.whiteContainer}>
        <Image
          source={{ uri: item.imageUrl }}
          style={styles.image}
          resizeMode="contain"
        />
      </View>
      <Text style={styles.placeholderText}>{`0/${item.setTotal}`}</Text>
      <Text style={styles.placeholderText}>Set Value: $0.00</Text>
    </TouchableOpacity>
  );

  const renderFooter = () =>
    isLoading && (
      <View style={styles.loadingContainer}>
        <ActivityIndicator size="small" color="gray" />
      </View>
    );

  return (
    <View style={{ flex: 1, backgroundColor: colors.light }}>
      <FlatList
        data={images}
        renderItem={renderImageItem}
        keyExtractor={(_, index) => index.toString()}
        numColumns={2}
        contentContainerStyle={styles.container}
        onEndReached={fetchImages}
        onEndReachedThreshold={0.5}
        ListHeaderComponent={
          <View style={styles.searchAndButtonsContainer}>
            <View style={styles.searchContainer}>
              <TextInput
                style={styles.searchBar}
                onChangeText={setSearchText}
                value={searchText}
                placeholder="Search for a set..."
                placeholderTextColor="gray"
              />
            </View>
          </View>
        }
        ListFooterComponent={renderFooter}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 5 + CONTAINER_PADDING,
    alignItems: "center",
    backgroundColor: colors.light,
  },
  imageContainer: {
    width: CONTAINER_WIDTH,
    height: CONTAINER_WIDTH + 40,
    padding: 5,
    marginBottom: -15,
    marginTop: 45,
    marginLeft: CONTAINER_PADDING / 2,
    marginRight: CONTAINER_PADDING / 2,
    alignItems: "center",
    justifyContent: "center",
  },
  whiteContainer: {
    width: "100%",
    height: "70%",
    borderRadius: 15,
    backgroundColor: "white",
    alignItems: "center",
    justifyContent: "center",
    borderWidth: 1,
    borderColor: colors.primary,
  },
  image: {
    width: "90%",
    height: "90%",
  },
  placeholderText: {
    marginTop: 5,
    color: colors.primary,
  },
  searchAndButtonsContainer: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    paddingHorizontal: 8,
    marginTop: 0,
  },
  searchContainer: {
    width: width * 0.8 - 10,
    flexDirection: "row",
    alignItems: "center",
    height: 38,
    borderColor: "gray",
    borderWidth: 1,
    borderRadius: 5,
    paddingHorizontal: 8,
    marginRight: 10,
  },
  searchBar: {
    flex: 1,
    height: 38,
    color: colors.primary,
  },
});

App.js

import React, { useState, useEffect } from "react";
import { Amplify, Storage } from "aws-amplify";
import { NavigationContainer } from "@react-navigation/native";
import AppNavigator from "./AppNavigator";
import LoadingScreen from "./LoadingScreen";
import awsmobile from "./src/aws-exports";

Amplify.configure(awsmobile),
  {
    Auth: {
      identityPoolId: IDENTITYPOOLID,
      region: "us-east-2",
    },
    Storage: {
      AWSS3: {
        bucket: BUCKET_NAME,
        region: "us-east-2",
        level: "public",
      },
    },
  };

Storage.configure({
  customPrefix: {
    public: "",
  },
});

function AppComponent() {
  const [isLoading, setIsLoading] = useState(true);

  // 模拟加载
  useEffect(() => {
    setTimeout(() => {
      setIsLoading(false);
    }, 3000); // 3秒延迟
  }, []);

  if (isLoading) {
    return <LoadingScreen />;
  }
  return (
    <NavigationContainer>
      <AppNavigator />
    </NavigationContainer>
  );
}

export default AppComponent;

S3桶策略

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::FakeBucketARN/*"
        }
    ]
}

内容的提问来源于stack exchange,提问作者rileylivingston

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 00:02:01