如果你是在做 React Native 图片加载优化,尤其是「大图卡顿 + 列表图片 + 缓存」,核心不是单纯换一个 Image 组件,而是要同时处理:
图片尺寸 → 解码 → 内存 → 磁盘缓存 → 列表复用 → 网络加载
1. 大图卡顿的核心原因
比如服务器返回一张 4000 × 3000 的 JPEG:
- 文件可能只有 2 MB
- 但解码到内存后大约是:
4000 × 3000 × 4 ≈ 48 MB
如果一个 FlatList 同时出现 5 张这样的图片,就可能瞬间产生 200+ MB 的 Bitmap/RGBA 内存压力。
所以:
不要把"压缩文件大小"和"降低图片内存占用"混为一谈。
最有效的优化通常是让服务端根据显示尺寸返回合适分辨率。
2. 首选:服务端图片缩放
假设 UI 中图片实际只显示:
屏幕:375px
图片:150 × 150
不要请求:
yaml
4000 × 4000
而应该请求类似:
300 × 300
或者根据 DPR:
ini
150 × 150 × 2 = 300 × 300
如果你的图片 CDN 支持参数,可以设计成:
ini
/image/xxx.jpg?w=300&h=300&q=80
甚至:
ini
/image/xxx.jpg?w=600&h=600&dpr=2
推荐尺寸策略
| UI 图片尺寸 | 推荐下载尺寸 |
|---|---|
| 50×50 | 100×100 |
| 100×100 | 200×200 |
| 150×150 | 300×300 |
| 300×200 | 600×400 |
| 全屏图片 | 根据设备实际分辨率 |
这通常是 RN 图片性能优化里收益最大的一步。
3. React Native 中不要只用 <Image>
如果是大量图片、Feed、聊天列表、瀑布流,我更推荐考虑:
expo-image
如果你使用 Expo:
ini
import { Image } from 'expo-image';
<Image
source={{ uri: imageUrl }}
style={styles.image}
contentFit="cover"
cachePolicy="memory-disk"
/>
缓存策略:
ini
cachePolicy="memory"
适合:
图片很容易重复出现,但不希望磁盘缓存太多。
ini
cachePolicy="disk"
适合:
希望图片能够跨 App 重启继续使用缓存。
ini
cachePolicy="memory-disk"
通常比较适合 Feed / 商品列表 / 社交 App。
还可以使用:
ini
placeholder={blurhash}
transition={200}
改善用户感知上的加载体验。
4. 如果是纯 React Native
可以使用:
ini
<Image
source={{ uri: url }}
style={styles.image}
/>
但需要注意:
RN Image 自身的缓存行为并不等于一个完整的图片缓存系统。
如果你对缓存有比较严格的需求,可以考虑:
expo-imagereact-native-fast-image- 自己实现 HTTP/CDN 缓存策略
不过现在如果是新项目,我通常会优先考虑 expo-image,尤其是 Expo 项目。
5. FlatList 才是第二个大坑
很多时候用户说:
"图片加载卡顿"
实际上真正的问题是:
FlatList
↓
同时创建大量 Image
↓
大量图片 decode
↓
JS / UI / Native 内存压力
↓
掉帧
例如:
ini
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
可以进一步优化:
ini
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
initialNumToRender={8}
maxToRenderPerBatch={6}
windowSize={5}
removeClippedSubviews
/>
但这些参数不要盲目调得很小。
例如:
ini
windowSize={2}
虽然内存下降,但快速滑动时可能频繁创建/销毁内容,反而出现闪烁和重新加载。
6. 图片组件一定要固定尺寸
不推荐:
css
<Image
source={{ uri: url }}
style={{
width: '100%',
aspectRatio: 1,
}}
/>
然后让布局不断计算。
更推荐让 FlatList item 的尺寸尽可能明确:
ini
const IMAGE_SIZE = 120;
<Image
source={{ uri: item.image }}
style={{
width: IMAGE_SIZE,
height: IMAGE_SIZE,
}}
/>
尤其是:
- Grid
- Avatar
- 商品列表
- 聊天列表
- 瀑布流
明确尺寸可以减少 layout 和渲染的不确定性。
7. 缓存策略不要简单理解成"永久缓存"
比较合理的是:
markdown
Memory Cache
↓
Disk Cache
↓
Network
例如:
第一次打开
Network
↓
Disk Cache
↓
Memory Cache
第二次进入页面
Memory Cache
↓
直接显示
App 重启
Disk Cache
↓
读取
缓存不存在 / 过期
Network
对于头像、商品图片、Feed 图片,可以采用不同策略。
Avatar
Memory + Disk
因为重复率很高。
Feed 图片
Memory + Disk
但要限制磁盘缓存规模。
一次性大图
Disk
甚至:
不长期缓存
8. URL 一定要稳定
缓存最怕这种情况:
ini
https://cdn.xxx.com/a.jpg?t=123
https://cdn.xxx.com/a.jpg?t=456
https://cdn.xxx.com/a.jpg?t=789
虽然实际是同一张图片,但缓存系统可能认为是不同资源。
更好的方式:
ini
https://cdn.xxx.com/image/a.jpg?v=abc123
内容发生变化的时候才修改版本:
ini
a.jpg?v=abc124
这样:
vbnet
URL
↓
Cache Key
↓
Image
能够稳定复用。
9. 大图不要在 RN 里面 resize
这是一个很重要的架构问题。
❌ 不推荐:
css
服务器 5000×5000
↓
RN 下载
↓
RN decode
↓
RN resize
↓
显示 300×300
因为最昂贵的事情之一已经发生了:
yaml
5000×5000 decode
应该:
css
原图
↓
CDN Resize
↓
300×300
↓
RN download
↓
decode
↓
显示
这样能同时减少:
- 网络流量
- decode 时间
- Bitmap 内存
- GC 压力
- UI 卡顿
10. 不要 preload 太多大图
例如:
ini
images.forEach(image => {
Image.prefetch(image.url);
});
如果 images 有 100 张:
100 × 大图
可能导致:
- 网络请求暴增
- 磁盘缓存暴增
- 内存压力
- decode 峰值
- 页面真正显示时反而卡
更合理的是:
diff
当前屏幕
+
前后 1~2 屏
进行有限预加载。
11. 一个比较成熟的架构
如果是 Feed / 电商 / 社交 App,我会建议:
arduino
┌──────────────┐
│ Image CDN │
└──────┬───────┘
│
width/height/q
│
▼
┌──────────────┐
│ expo-image │
└──────┬───────┘
│
┌─────────┴─────────┐
▼ ▼
Memory Cache Disk Cache
│ │
└─────────┬─────────┘
▼
FlatList
│
Virtualization
│
▼
Visible Items
核心原则就是:
让"需要显示多少像素"决定"下载多少像素",而不是让原图尺寸决定一切。
12. 如果你的问题是"滚动时卡顿"
我会按这个优先级排查:
arduino
① 图片是不是远大于显示尺寸
↓
② 是否使用了 CDN resize
↓
③ 图片 decode 是否占用大量内存
↓
④ FlatList 是否一次渲染太多 item
↓
⑤ Image 是否频繁 mount/unmount
↓
⑥ cache 是否命中
↓
⑦ 是否存在 JS thread 阻塞
↓
⑧ 是否存在 UI thread / GPU 压力
不要第一时间就调 windowSize / initialNumToRender。
很多项目把 FlatList 参数调了一圈,最后发现真正的问题是:
yaml
每个 100×100 的头像
实际上下载的是 3000×3000