我在微信开发者工具里检查网络请求时,发现一个弹窗里的图片已经下载了,但弹窗从未打开。继续往下查,又发现轮播第一页出现后,其余几张图片也在同一时间请求。
图片格式和尺寸都没有问题。真正的问题是组件已经挂载,只是用户暂时看不见。
这类请求很容易漏掉。界面看起来只有一张图,代码也写了 opacity-0 或 lazyLoad,网络面板却是另一回事。排查时应先确认 <Image> 是否存在于组件树,再讨论缓存、WebP 或 CDN。
透明隐藏仍会创建 Image
下面的弹窗一直挂载。visible 只改变样式:
function ImageModal({ visible, imageUrl, onClose }: Props) {
return (
<View
className={
visible
? "fixed inset-0 opacity-100"
: "fixed inset-0 opacity-0 pointer-events-none"
}
onClick={onClose}
>
<Image src={imageUrl} mode="aspectFit" />
</View>
);
}opacity: 0 只影响绘制,pointer-events: none 只影响点击。两者都不会卸载 <Image>,所以运行时仍可能请求 imageUrl。
如果图片只在弹窗打开后才有用,直接控制挂载:
function ImageModal({ visible, imageUrl, onClose }: Props) {
if (!visible) return null;
return (
<RootPortal>
<View className="fixed inset-0" onClick={onClose}>
<Image src={imageUrl} mode="aspectFit" />
</View>
</RootPortal>
);
}关闭时组件返回 null,打开后才创建 <Image>。这时请求时机和用户操作是一致的。
这个改法会取消原来依赖隐藏状态完成的退出动画。如果必须保留动画,可以在动画结束后再卸载内容,但不要为了动画让网络资源长期留在组件树里。
Swiper 会渲染看不见的子项
轮播的情况更隐蔽。页面只显示当前项,不代表其他 SwiperItem 没有渲染:
<Swiper circular>
{images.map((url) => (
<SwiperItem key={url}>
<Image src={url} mode="aspectFill" />
</SwiperItem>
))}
</Swiper>如果 images 有八项,上面的代码就创建八个 <Image>。是否能看到某一项,不会改变它已经挂载的事实。
一种直接的处理方式是只挂载当前项和相邻项:
function getAdjacentIndexes(index: number, length: number) {
if (length <= 0) return new Set<number>();
if (length === 1) return new Set([0]);
return new Set([
index,
(index - 1 + length) % length,
(index + 1) % length,
]);
}组件中维护已经允许加载的索引:
const [loadedIndexes, setLoadedIndexes] = useState(
() => getAdjacentIndexes(0, images.length),
);
function loadAdjacentImages(index: number) {
setLoadedIndexes((current) => {
const next = new Set(current);
getAdjacentIndexes(index, images.length).forEach((item) => next.add(item));
return next;
});
}渲染时,没有进入集合的项只保留占位:
<Swiper
circular
onChange={(event) => loadAdjacentImages(event.detail.current)}
>
{images.map((url, index) => (
<SwiperItem key={url}>
{loadedIndexes.has(index) ? (
<Image src={url} mode="aspectFill" />
) : (
<View className="h-full bg-black" />
)}
</SwiperItem>
))}
</Swiper>圆形轮播在第一页会准备末尾、当前和下一项,初次最多加载三张。用户滑动后,再把新的相邻项加入集合。已经加载过的索引不删除,回滑时不会因为组件反复卸载而重新闪烁。
如果图片数量很多,也可以只保留当前窗口,离开后卸载远处图片。这样更省内存,但回滑时可能重新请求或重新解码。是否需要做到这一步,要看图片数量和小程序的实际内存表现。
全屏预览不要再换一套图片
自定义全屏预览通常又有一个 Swiper。如果它在打开时直接渲染全部图片,前面的限制就失效了。
预览层应在打开后才挂载,并沿用普通轮播的 loadedIndexes。当前图片如果已经下载过,预览也尽量复用同一个 URL。为了全屏查看再生成另一套地址,往往会让点击动作触发第二次下载。
如果全屏必须使用更高清的资源,可以只替换当前项,等用户滑动后再加载新的当前项。不要在打开预览的一刻把整组高清图同时挂上去。
视频也要单独检查。autoplay={false} 只表示不自动播放,不一定表示组件不会读取封面、元数据或媒体内容。未进入加载窗口的视频最好连 <Video> 一起延后挂载。
lazyLoad 不是弹窗开关
Taro 的 <Image lazyLoad /> 适合滚动列表,但它不能表达“用户点击后才需要”。
固定定位的弹窗、透明容器和轮播子项是否进入懒加载判定范围,容易受到布局和运行时实现影响。既然代码已经有 visible、show 或 isOpen,就用这些状态控制组件是否存在。
我现在把两件事分开处理:
- 滚动列表使用
lazyLoad,减少尚未进入视口的图片请求。 - 弹窗、抽屉和全屏预览使用条件渲染,保证交互发生前没有媒体组件。
用网络面板验证
这类修改不能只看页面。微信开发者工具的 Network 面板更直接:
- 清空请求记录并重新进入页面。
- 不点击弹窗,确认弹窗图片没有请求。
- 查看轮播初次产生多少图片请求。
- 滑动一项,确认只增加新的相邻图片。
- 打开全屏预览,检查当前图片是否复用了原 URL。
- 关闭预览后再次打开,观察缓存和组件挂载是否符合预期。
还要留意请求发起者和 URL。相同内容如果带了不同尺寸、签名或查询参数,在缓存看来就是不同资源。
界面不可见和资源未加载是两回事。判断是否省下请求,最后还是要回到组件挂载状态和网络记录。