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
相关产品推荐
相关产品推荐

