Engineering
Fast Image Feeds Are a Scheduling Problem
Image-heavy feeds feel fast when the browser does the right work in the right order. A practical guide to image sizing, loading, caching, and scroll UX.

How to make media-heavy feeds feel fast by controlling the order in which the browser discovers, downloads, and renders their content.
Image-heavy feeds often begin to feel slow before anything particularly dramatic happens on the network. The browser discovers the important images too late, requests variants that are much larger than their rendered slots, waits for missing geometry, and spends resources on distant content while the first screen remains incomplete. Compression helps with transfer size, although the experience itself is usually decided by the order of work.
This makes a feed a scheduling system as much as a collection of media. The first viewport needs to become visually complete, the next viewport needs to be prepared before scrolling reaches it, and everything farther away can remain dormant until its turn comes. Once those distances are treated differently, the browser has a much better chance of doing useful work first.
The implementation may currently involve server-rendered HTML, srcset, native lazy-loading, IntersectionObserver, a CDN, and a few browser priority hints. Those details will evolve. The underlying UX problem will remain: every resource enters a queue, and the quality of the feed depends on which resources reach the front.
The path from URL to pixels
An image passes through several stages before it appears inside a card. The browser has to discover the URL, choose a source, assign a priority, transfer the file, decode it, and place the resulting pixels into a known area of the layout. Delays at any of these stages can leave a visible hole even when the image server itself is fast.
Stage | Browser question | Typical failure |
|---|---|---|
Discovery | Which images exist on this page? | Important URLs appear only after JavaScript and an API request |
Selection | Which available source fits the rendered slot? | A large original is selected for a small card |
Priority | Which requests should move first? | Visible media competes with offscreen content |
Transfer | Can the file be served nearby or reused? | The browser repeatedly downloads an unchanged asset |
Decode | When can the compressed file become pixels? | Several large images finish together and create main-thread work |
Present | Where should the image appear? | The browser learns the card dimensions after loading begins |
Looking at this complete path is more useful than treating every slow image as a compression problem. A smaller file can still arrive late, decode at an inconvenient moment, or push the rest of the layout around when it appears.
Three practical distances
A feed becomes easier to reason about when its content is divided by distance from the current viewport. The first group contains what the person can already see. The second contains the next one or two viewports. The third contains the rest of the feed, which may never be reached during the session.
Visible content should be discoverable early, rendered into reserved space, and given enough priority to complete the first screen. Nearby content can begin loading quietly in the background, with enough lead time to be ready when scrolling reaches it. Distant content needs almost nothing yet.
The exact boundaries depend on the product. A compact marketplace grid may prepare several rows at once, while a feed of large editorial posters may need a smaller batch because every card carries more bytes and more decoding work. Scroll velocity, API latency, image weight, device class, and connection quality all affect the useful preparation distance.
This is why a fixed instruction such as “lazy-load every image after the first six” rarely survives contact with a real product. The more durable decision concerns distance and urgency. The number of cards follows from that.
Make the first viewport discoverable
Many feeds introduce their first images through a long client-side chain:
By the time the browser learns which images matter, it has already downloaded, parsed, and executed the code required to reveal them. A fast CDN can shorten the final request, but it cannot recover the time spent before discovery.
Rendering the initial cards into the HTML response gives the browser an earlier work order. That HTML may come from server rendering, static generation, streaming, or another architecture; the relevant part is that the image URLs are visible during the first parse.
The probable Largest Contentful Paint image should load eagerly. A fetchpriority="high" hint can help the browser recognise its importance sooner, especially when several images appear near the top of the document. Applying the hint to one or two critical resources preserves the distinction that the hint is supposed to communicate.
Current browser guidance recommends exposing the LCP resource in the initial HTML and avoiding loading="lazy" on it. The implementation advice may change as browser heuristics improve, while early discovery will continue to matter because the browser can only prioritise resources it knows about. The web.dev LCP guide describes this part of the loading path in more detail.
Match image dimensions to the rendered slot
A product may store a 2048px source because the image also appears in a lightbox, detail page, or high-density display. The grid card still needs a source appropriate for its own dimensions.
Responsive image markup gives the browser a set of derivatives and a description of the slot:
srcset lists the available files and their intrinsic widths. sizes describes the expected rendered width of the slot at each breakpoint. The browser uses both values, together with display density and its own selection logic, to choose a source.
The fragile part is usually sizes, because the layout continues to change after the image component has been written. A four-column grid becomes five columns, gaps are adjusted, or a sidebar is added, while the old slot description survives untouched. The markup still looks responsive, although the browser is making decisions from obsolete information.
The selected file can be checked through currentSrc:
Comparing currentSrc, clientWidth, naturalWidth, and the device pixel ratio usually reveals systematic over-delivery quickly. The goal is a sensible match across common conditions rather than mathematical perfection for every possible viewport.
Modern formats remain useful because they can reduce transfer size at comparable visual quality. Their effect compounds with correct dimensions. A well-sized WebP or AVIF variant has a much easier job than a full-resolution source being compressed into a small CSS box. MDN’s responsive image guide explains how srcset and sizes participate in source selection.
Reserve the geometry before the pixels arrive
A feed can finish loading quickly and still feel unstable when every arriving image changes the page geometry. Cards move, text shifts, and the scroll position drifts under the pointer. The interface keeps renegotiating its shape during the exact moment when someone is trying to understand it.
An aspect-ratio container gives the browser a stable box from the beginning:
Explicit width and height attributes provide the intrinsic ratio at the HTML level, allowing the browser to reserve space before the image has been transferred and decoded. CSS can still change the rendered dimensions while preserving that ratio.
The placeholder mainly needs to hold the layout together. A neutral colour often handles this well. Low-resolution previews can create a smoother visual transition when the imagery itself carries the product experience, although they introduce another representation that has to be generated, delivered, decoded, and replaced. Animated shimmer across an entire grid adds constant motion and can make the page feel busier during loading.
Accessibility remains part of the image contract. Content images need useful alternative text, while decorative images can use alt="". Performance attributes should not change the semantic role of the image.
Use lazy-loading according to location
Native lazy-loading works well for ordinary images below the initial viewport because the browser already understands its layout and current resource pressure. An image that remains far outside the visible area can wait, and an image that is never reached may avoid the request entirely.
Using JavaScript to reproduce basic image lazy-loading can delay discovery further when real URLs are hidden inside data-src attributes. It also adds code and main-thread work around a capability already available in current browsers. JavaScript still has a role when the product needs custom loading behaviour, but ordinary offscreen <img> elements rarely need an entire loading framework.
The decoding="async" attribute gives the browser a preference about image decoding. Its visible effect may be small for static images, so it belongs in the category of useful hints rather than foundational fixes. When an interface replaces one image with another dynamically, HTMLImageElement.decode() provides more direct control:
The replacement happens after the new file is ready to render, reducing the chance of an empty frame during the swap. MDN documents decode() and the situations where it is useful.
Prepare the next batch before the boundary appears
Native lazy-loading schedules individual files, while infinite pagination decides when the product should request another group of cards. Waiting until the final loaded row reaches the viewport exposes the API latency, rendering time, and image discovery delay in sequence.
A sentinel placed near the end of the loaded feed can trigger the next page earlier:
The loading guard matters because a moving sentinel can otherwise create overlapping requests. Those requests may return duplicate pages, render several batches together, and introduce a large group of image downloads at the same moment. The feed creates congestion while trying to stay ahead.
The rootMargin controls the preparation distance. Its useful value depends on scroll behaviour, API latency, batch size, image weight, and the devices on which the product is used. Testing should find the smallest margin that keeps the boundary out of sight under normal conditions.
Fetching many pages in advance makes the loading gap less likely, although it also increases wasted transfer, decoding work, memory use, and competition with current interactions. Backpressure keeps anticipation bounded.
Give stable files stable identities
Generated image variants work well with long cache lifetimes when every URL identifies a particular file:
The response can then be cached aggressively:
When the underlying image changes, the asset receives a new URL. The browser and CDN can safely continue reusing the old representation because its identity still points to the old file.
Teams often shorten cache lifetimes when a URL can silently change its contents. Every visit then pays for uncertainty in the asset pipeline. Content-addressed or versioned URLs remove that ambiguity and allow the caching layer to behave predictably.
The immutable directive has formal HTTP semantics described in RFC 8246. The broader principle is simply that a stable representation needs a stable identity.
Long sessions introduce DOM and memory pressure
A feed may perform well during initial loading and gradually become less responsive after substantial scrolling. At that stage the page contains far more than image requests: thousands of DOM nodes, layout boxes, observers, event handlers, decoded bitmaps, and retained component state may all remain active.
Native lazy-loading reduces early network work, while previously rendered cards still accumulate in the document. Style calculations and DOM mutations become more expensive as the feed grows. Returning to a background tab can also reveal how much memory the session has retained.
The first response should usually be measured restraint: reasonable page sizes, bounded prefetching, efficient card components, and careful cleanup of observers or listeners. content-visibility can reduce rendering work for large offscreen sections. Full list virtualization becomes useful when long-session measurements show that DOM size is the actual constraint.
Virtualization changes product behaviour and deserves testing beyond frame rate. Browser search, accessibility, scroll restoration, anchors, variable card heights, and returning from a detail page can all become more complicated when offscreen items are removed from the DOM. web.dev’s long-list guide describes the performance pressure that leads products toward this technique.
Loading states should preserve the feed
A useful loading state shows where content will arrive without replacing the surrounding interface. Stable card shells work well because the feed structure remains visible and already loaded content stays interactive.
Image failures should remain local to the affected card. The card can keep its dimensions, metadata, and actions while offering a retry or fallback inside the media area. Restarting the entire feed for one failed image destroys context and makes a small network problem feel like a product failure.
Scroll restoration matters for the same reason. Someone who opens a card and returns expects the previous position and loaded content to remain available. Reconstructing the feed from the beginning may technically load quickly, but it discards the state the person had already built through navigation.
Image reveal animation also deserves restraint. A brief opacity transition can soften a dynamic replacement, while a long fade delays content that has already completed its network and decoding work. Applying the same entrance animation to every card means the effect repeats throughout the entire session.
Measure the experience across the whole feed
Lab tools such as Lighthouse and browser performance traces make controlled comparisons possible. Field measurements show how the feed behaves across real devices, connections, cache states, viewport sizes, and scrolling patterns. Both views are useful for different reasons.
Core Web Vitals cover part of the experience. LCP can expose a late or poorly prioritised first image, CLS catches unstable geometry, and INP can reveal contention between rendering work and interaction. A feed also needs measurements that reflect its own interaction model.
First viewport completion measures the point at which the visible cards contain their intended media rather than placeholders. LCP only describes the largest qualifying element, so several neighbouring cards may remain empty after LCP has already been recorded.
Blank exposure measures how often scrolling reaches an unloaded card or the end of the current batch. This connects directly to the preparation distance and pagination schedule. Useful bytes measure how much image data was transferred before the first screen became complete, which helps reveal distant resources competing with visible ones.
Selected-source accuracy compares the rendered dimensions with the files chosen through currentSrc. Long-session measurements cover memory, DOM size, frame stability, and interaction after substantial scrolling. A clean initial load can hide problems that only appear after the feed has been used for several minutes.
Field data should be read as a distribution rather than a single universal number. web.dev’s comparison of field and lab data gives a useful account of why the two views differ.
A review sequence for image-heavy feeds
A practical review starts by tracing the first visible image from HTML discovery to presentation. Check when its URL becomes available, whether it is loaded eagerly, which source the browser chooses, when its geometry becomes known, and what other requests compete with it.
The next pass should follow the feed during movement. Scroll quickly enough to expose the pagination boundary, inspect overlapping requests, and watch which images begin loading before they are useful. Slow connections and mid-range mobile devices tend to reveal scheduling mistakes more clearly than a local desktop connection.
The final pass should cover time and failure. Continue scrolling until the DOM has grown substantially, open an item and return, suspend and restore the tab, fail several image requests, and repeat the flow with warm and cold caches. This shows whether the feed remains coherent after the ideal first-load scenario has ended.
The resulting order of work is usually straightforward: expose the first images early, serve variants that fit their slots, reserve their geometry, defer distant media, prepare the next batch with bounded lead time, and control the cost of long sessions. Formats, frameworks, and browser attributes can change around that sequence without changing the method itself.





