图片上传后还在转圈?试试这几种预加载方案解决显示延迟问题
图片上传完,用户点提交,评论发出去了,图片还在转圈。这体验实在太难受了。
最近在做评论组件的图片拖拽上传功能,功能很快就搞定了,但上线后踩了个坑:图片上传到 CDN 之后,评论显示出来的那一瞬间,图片还没加载完,用户看到的就是一片空白,然后再慢慢浮现。
解决办法其实不复杂:上传完成之后偷偷预加载一下,等用户提交评论的时候,图片已经在浏览器缓存里了。但具体怎么"偷偷预加载",我发现还真不止一种写法,而且每种都有各自的坑。
new Image()
最直观的写法,创建一个 Image 对象,设置一下 src,浏览器就会去下载:
const img = new Image();
img.src = "https://example.com/image.png";可以监听加载完成,还能提前拿到图片尺寸:
const img = new Image();
img.src = url;
img.onload = () => {
console.log('图片加载完成!');
console.log({
height: img.naturalHeight,
width: img.naturalWidth
});
};我做了个简单测试:页面加载时预加载,2秒后显示图片。
<img src="" id="imageEl" style="display: none" />
<script>
const imageEl = document.getElementById("imageEl");
const url = "http://localhost:3000/image.png";
const img = new Image();
img.fetchPriority = "high";
img.src = url;
setTimeout(() => {
imageEl.src = url;
imageEl.style.display = "block";
}, 2000);
</script>效果挺好,图片瞬间弹出,Network 面板里只有一个请求。
但这里有个坑(也是我最开始没想到的):这种方式依赖浏览器的 HTTP 缓存。如果服务器返回了 Cache-Control: no-store,缓存就不生效了。
// 服务器端设置
app.get('/image.png', (_req, res) => {
res.setHeader('Cache-Control', 'no-store');
res.sendFile(path.join(__dirname, 'image.png'));
});这时候你会看到两个请求,图片还是会延迟加载。
所以 new Image() 虽然简单,但真不是100%可靠。如果你的 CDN 配置了 no-store(很多安全要求高的服务确实会这么配),这一招就废了。
link rel="preload"
浏览器原生支持的声明式预加载。HTML 里加一行:
<head>
<link rel="preload" href="https://example.com/image.png" as="image">
</head>用 JavaScript 动态创建也行:
const link = document.createElement("link");
link.rel = 'preload';
link.as = 'image';
link.href = "https://example.com/image.png";
link.fetchPriority = "high"; // 动态注入默认是低优先级,要手动拉高
document.head.append(link);重点来了:即使服务器设置了 no-store,Network 面板里还是只有一个请求,图片瞬间显示。
原因也不复杂:preload 不走 HTTP 缓存,它有一套自己的 preload cache。图片需要渲染的时候,浏览器会先查 preload cache,直接从那里读。
那如果预加载还没完成,图片就要显示了,会不会新开一个请求?
我模拟了 3G 网络测试,发现浏览器比我想的聪明:预加载正在进行中的话,它会等那个请求完成,不会另开一个。图片虽然还是延迟显示了,但至少没浪费流量。这点确实省心。
隐藏 div + CSS background-image
偶尔能在网上看到的写法,创建一个隐藏的 div,设置背景图:
const div = document.createElement("div");
div.style.backgroundImage = "url('http://localhost:3000/image.png')";
div.style.visibility = "hidden";
div.style.position = "absolute";
document.body.appendChild(div);会触发高优先级请求。但有个要注意的地方:不能用 display: none,否则浏览器根本不下载图片。
因为 display: none 会把元素从文档流中移除,浏览器不渲染它,自然也不会去下载背景图。
说实话,这个方案我实在想不出什么时候比前两个更好。可能是历史遗留的骚操作吧。
Cache API
现代浏览器原生的 Cache API,可以手动管理缓存:
const url = "http://localhost:3000/image.png";
const cache = await caches.open('images');
await cache.add(url);
const cachedResponse = await cache.match(url);好处是返回 Promise,可以精确控制执行顺序:
const url = "http://localhost:3000/image.png";
const cache = await caches.open('images');
// 等缓存写完再往下走
await cache.add(url);
setTimeout(async () => {
const response = await cache.match(url);
const blob = await response.blob();
const fetchedUrl = URL.createObjectURL(blob);
imageEl.src = fetchedUrl;
imageEl.style.display = 'block';
}, 2000);即使网络慢,也能确保图片完全加载后才显示。
但有个麻烦事:不会自动清理。我测试的时候刷新了几次页面,缓存堆积了不少。所以得手动删:
setTimeout(async () => {
const response = await cache.match(url);
const blob = await response.blob();
const fetchedUrl = URL.createObjectURL(blob);
imageEl.src = fetchedUrl;
imageEl.style.display = 'block';
cache.delete(url); // 用完即删
}, 3000);忘了清理的话,缓存会一直堆着(浏览器最终会清,但谁知道它什么时候动手)。
fetch()
如果你喜欢 Cache API 的控制力,但不想管缓存清理,fetch 也能干类似的事:
const res = await fetch("http://localhost:3000/image.png");
const blob = await res.blob();
const fetchedUrl = URL.createObjectURL(blob);
setTimeout(async () => {
imageEl.src = fetchedUrl;
imageEl.style.display = 'block';
}, 3000);fetch 和 Cache API 都能自定义请求头、直接操作响应数据。但 fetch 同样受 Cache-Control 头影响,响应带了 no-store 就会重新请求。
对我来说这不是问题(只需要短时间用一下),但其他场景下可能会踩到。
怎么选
对于我的场景(评论组件里偷偷预加载),link rel="preload" 是最合适的。不用管缓存策略,不用担心 no-store,也不用自己清理。
| 方案 | 什么时候用 |
|---|---|
| new Image() | 要监听加载事件或提前拿图片尺寸 |
| link rel="preload" | 想可靠预加载、不想操心缓存 |
| 隐藏 div + background-image | 实在想不出什么时候比前两个更好 |
| Cache API | 要精细控制缓存,或跨页面保持资源 |
| fetch() | 类似 Cache API 但只需要短时间内存使用 |
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!