图片明明已经加载完成,但在页面上仍然会“从上到下一点点出现”,看起来像在流式加载。
在做网页全屏背景图时,我遇到一个很影响体验的问题:
图片明明已经加载完成,但在页面上仍然会“从上到下一点点出现”,看起来像在流式加载。尤其是大图或网络稍慢时,这种效果会让页面显得很廉价,即使实际上资源已经提前加载过。
于是我尝试了几种不同的优化方式,想弄清楚一件事:如何让图片在“可见的瞬间”是完整的,而不是渐进式出现?
最直觉的做法是预加载:
javascript
逻辑很简单:等图片下载完成,再展示页面。按理说,这样 <img> 渲染时应该是“瞬间出现”的。
但实际效果仍然存在“从上到下逐步显现”。
关键点在于:onload 只保证“网络下载完成”,但不保证“解码完成”或“渲染完成”。
浏览器图片管线大致是:
下载完成 → 解码 → 栅格化 → 绘制
而“逐步出现”的现象,往往发生在解码 + 绘制阶段,而不是下载阶段。
我尝试使用:
html
理论上它可以让浏览器在解码时同步执行,避免异步调度带来的闪烁。但结果是:依然存在渐进式显示问题。
查阅 MDN 后可以确认:decoding 控制的是“解码是否阻塞渲染调度”,而不是“图片是否逐步显示”。也就是说它影响的是主线程是否等待 decode 完成,而不是图片是否渐进渲染。
而“从上到下出现”的现象,本质通常来自:
因此,decoding 并不能控制“图片第一次可见的完整性”。
下一步思路变成:既然问题出在 decode + render,那就等它完全 ready 再插入 DOM。
jsx
MDN 对 decode() 的定义是:Promise resolves once image data is ready to be used。理论上这是最接近“完全准备好再显示”的 API。
但问题出现了:为什么还是有延迟?甚至出现了“页面先空一段时间,再突然出现图片”的情况。并且 Network 面板发现:图片请求发生了第二次。
进一步排查后发现关键点:开发环境(Vite dev server)返回:
Cache-Control: no-cache
ETag: ...
这里的重点是:no-cache 并不是“不缓存”,而是“每次使用前必须验证”。
当 <img> 加载资源时:
所以即使你“预加载过”,也会发生一次额外的网络往返。这就导致:
<img> 并没有真正共享“即时可用状态”既然问题在于网络 + 缓存验证链路无法被完全消除,那就直接跳过它。
把图片从“URL资源”变成“内存资源”:
javascript
然后:
jsx
Blob URL 的特点是:
当然也有代价:
URL.revokeObjectURL本质是:用“内存确定性”换“网络不确定性”。
URL
↓
Cache lookup(可能触发 revalidate)
↓
Network fetch
↓
Image decode
↓
Rasterize / GPU upload
↓
Paint