The image is clearly already loaded, but on the page it still appears to "show up gradually from top to bottom," looking as if it's being streamed in.
When creating a full-screen background image for a webpage, I encountered an issue that significantly affects user experience:
Even though the image has clearly finished loading, it still appears on the page “gradually from top to bottom,” looking like it’s being streamed in. Especially with large images or slightly slow networks, this effect makes the page feel cheap, even though the resource may have been loaded in advance.
So I tried several different optimization methods to figure out one thing: How can the image be complete at the “moment it becomes visible,” rather than appearing progressively?
The most intuitive approach is preloading:
javascript
The logic is simple: wait for the image download to finish before showing the page. In theory, the <img> should appear “instantly” when rendered.
But in practice, the “gradual appearance from top to bottom” still occurs.
The key point is: onload only guarantees that “network download is complete,” not that “decoding is complete” or “rendering is complete.”
The browser’s image pipeline roughly follows:
Download complete → Decode → Rasterize → Paint
And the “gradual appearance” phenomenon usually happens during the decode + paint stage, not the download stage.
I tried using:
html
In theory, this forces the browser to decode synchronously, avoiding flicker caused by asynchronous scheduling. But the result was: the progressive display problem still existed.
According to MDN: decoding controls whether “decoding blocks rendering scheduling,” not whether “the image appears progressively.” That is, it affects whether the main thread waits for decode completion, not whether the image renders progressively.
The “appearance from top to bottom” phenomenon typically stems from:
Therefore, decoding cannot control the “integrity of the image when first visible.”
The next idea became: since the problem lies in decode + render, insert into DOM only after it’s fully ready.
jsx
MDN defines decode() as: Promise resolves once image data is ready to be used. Theoretically, this is the API closest to “show only after completely ready.”
But a problem arose: why was there still a delay? Even worse, the page stayed blank for a while before the image suddenly appeared. And the Network panel showed: the image request happened a second time.
Further investigation revealed the key point: the development environment (Vite dev server) returns:
Cache-Control: no-cache
ETag: ...
The important detail here is: no-cache does not mean “no caching,” but “must validate before each use.”
When <img> loads a resource:
So even if you “preloaded,” an extra network round-trip still occurs. This leads to:
<img> not truly sharing an “immediately available state”Since the problem lies in the fact that the network + cache validation chain cannot be fully eliminated, we simply skip it.
Turn the image from a “URL resource” into an “in-memory resource”:
javascript
Then:
jsx
The characteristics of Blob URL are:
Of course, there are trade-offs:
URL.revokeObjectURLEssentially: trading “memory certainty” for “network uncertainty.”
URL
↓
Cache lookup (may trigger revalidation)
↓
Network fetch
↓
Image decode
↓
Rasterize / GPU upload
↓
Paint