Zhongfarewell

How to Resolve Visual Fragmentation Caused by "Streaming Loading" of Large Images

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.

#How to Solve the Visual Fragmentation Caused by "Streaming Load" of Large Images

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?

#1. First Instinct: Use Preloading to Avoid Showing the Loading Process

The most intuitive approach is preloading:

javascript
const img = new Image(); img.onload = () => { showPage(); }; img.src = wallpaperUrl;

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.

#Why Didn’t Preloading Solve the Problem?

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.

#2. Second Attempt: Control Decoding Behavior (decoding)

I tried using:

html
<img src="wallpaper.jpg" decoding="sync" />

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.

#Why Wasn’t decoding Effective?

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:

  • Progressive JPEG encoding
  • Browser chunked decoding + multi-frame painting strategy

Therefore, decoding cannot control the “integrity of the image when first visible.”

#3. Third Attempt: Delay DOM Insertion (decode API)

The next idea became: since the problem lies in decode + render, insert into DOM only after it’s fully ready.

jsx
const [ready, setReady] = useState(false); useEffect(() => { const img = new Image(); img.src = wallpaperUrl; img.decode().then(() => { setReady(true); }); }, []); return ( <img src={wallpaperUrl} style={{ opacity: ready ? 1 : 0 }} /> );

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.

#4. The Real Issue: Cache Was Not Reused in the Way You Thought

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.”

#Actual Browser Behavior

When <img> loads a resource:

  1. Send a conditional request (If-None-Match)
  2. Wait for server to return 304
  3. Read from local cache
  4. Then decode + render

So even if you “preloaded,” an extra network round-trip still occurs. This leads to:

  • Preload and <img> not truly sharing an “immediately available state”
  • Still experiencing “blank waiting” in practice

#5. Final Solution: Bypass Network and Cache — Blob URL

Since the problem lies in the fact that the network + cache validation chain cannot be fully eliminated, we simply skip it.

#Core Idea

Turn the image from a “URL resource” into an “in-memory resource”:

javascript
const res = await fetch(wallpaperUrl); const blob = await res.blob(); const blobUrl = URL.createObjectURL(blob);

Then:

jsx
<img src={blobUrl} />

#What Happens Here?

The characteristics of Blob URL are:

  • No HTTP request is sent
  • It bypasses the cache layer
  • It does not trigger revalidation
  • Binary data is read directly from memory

#Result

  • No streaming display
  • No visible progressive rendering process
  • Image appears instantly

#Trade-offs

Of course, there are trade-offs:

  • Occupies memory
  • Cannot reuse HTTP cache
  • Requires manual URL.revokeObjectURL
  • CDN optimizations become ineffective

Essentially: trading “memory certainty” for “network uncertainty.”

#6. Looking Back: What Exactly Is the Browser Doing During Image Rendering?

#We Can Abstract the Entire Process As:

URL

Cache lookup (may trigger revalidation)

Network fetch

Image decode

Rasterize / GPU upload

Paint