How to Make Website Thumbnails Load Lazily in a Directory Page
Use native lazy loading for offscreen directory thumbnails, preserve their layout space, and keep initially visible images eager.
For thumbnails below the initial viewport, add loading="lazy" to each <img>. Keep images likely to appear immediately—including a hero or likely Largest Contentful Paint (LCP) image—eager, and reserve each thumbnail’s space with dimensions or an aspect ratio. The browser chooses when a lazy image is close enough to start loading; the attribute is a hint, not an exact pixel threshold.
1. Add native lazy loading to directory thumbnails
Put the attribute on the image itself. This runnable example marks a below-the-fold listing thumbnail as lazy and supplies its intrinsic dimensions:
<a href="/listing/example">
<img
src="/images/example-thumbnail.jpg"
alt="Example site preview"
width="320"
height="200"
loading="lazy"
>
</a>
Use dimensions that match the image’s intrinsic width and height when possible. They reserve layout space while the browser fetches the image, reducing layout movement. If the displayed crop differs, CSS can control its presentation:
.directory-thumbnail {
display: block;
width: 100%;
height: auto;
aspect-ratio: 8 / 5;
object-fit: cover;
}
Native lazy loading is widely available across browsers; MDN describes it as broadly available since March 2022. For ordinary offscreen images, it is usually the simplest starting point without adding a JavaScript library.
2. Keep initially visible images eager
Do not blindly mark every image lazy. Images in the first viewport should generally load eagerly, especially a prominent hero or another likely LCP candidate. Delaying an image the visitor needs immediately can make the page appear slower.
If shared code applies lazy loading to every image, exclude the images likely to be visible at realistic mobile and desktop viewport sizes. The right cutoff depends on the page layout and device; do not assume that the same number of rows is always below the fold. An image without loading="lazy" uses the default eager behavior.
<!-- Likely visible immediately: leave eager -->
<img src="/images/featured.jpg" alt="Featured listing" width="640" height="400">
<!-- Further down the directory: defer when appropriate -->
<img src="/images/listing-12.jpg" alt="Listing 12 preview" width="320" height="200" loading="lazy">
3. Use lazy loading with responsive images
When using <picture> with alternate formats or art-directed sources, put loading="lazy" on its child <img>, not on the <picture> element:
<picture>
<source srcset="/images/example-thumbnail.avif" type="image/avif">
<source srcset="/images/example-thumbnail.webp" type="image/webp">
<img
src="/images/example-thumbnail.jpg"
alt="Example site preview"
width="320"
height="200"
loading="lazy"
>
</picture>
For resolution switching, the same placement applies with srcset and sizes on the image:
<img
src="/images/example-640.jpg"
srcset="/images/example-320.jpg 320w, /images/example-640.jpg 640w"
sizes="(max-width: 600px) 100vw, 320px"
alt="Example site preview"
width="640"
height="400"
loading="lazy"
>
4. Add lazy thumbnails to a dynamically rendered directory
Set the property before appending each image, or as part of the markup before it becomes part of the document. Decide whether a particular item should be lazy based on whether it is likely to start below the initial viewport:
const listings = [
{ title: "Example site", image: "/images/example.jpg", href: "/listing/example" },
{ title: "Another site", image: "/images/another.jpg", href: "/listing/another" }
];
const grid = document.querySelector(".directory-grid");
listings.forEach((listing, index) => {
const link = document.createElement("a");
link.href = listing.href;
const img = document.createElement("img");
img.src = listing.image;
img.alt = `${listing.title} preview`;
img.width = 320;
img.height = 200;
img.loading = index < 4 ? "eager" : "lazy";
link.append(img);
grid.append(link);
});
The example keeps the first four items eager only as an illustration. Choose the cutoff from the actual layout and viewport sizes, not from a fixed rule. If items are inserted or reordered after rendering, set each image’s loading behavior when it is created.
5. Know what the browser controls
loading="lazy" asks the browser to defer fetching an image until it is sufficiently near the viewport. The browser decides that distance, and it can vary with browser behavior and conditions. The attribute does not let the page specify an exact number of pixels or a precise loading schedule.
Native lazy loading is appropriate when the requirement is simply to defer ordinary offscreen thumbnails. Use Intersection Observer if the interface needs custom behavior based on an element entering or leaving a chosen intersection region. Older scroll, resize, or orientation-change handlers can also implement custom loading, but require more code and event management.
6. Troubleshoot common problems
| Symptom | Likely cause | Fix |
|---|---|---|
| A visible thumbnail appears late. | An initially visible image was marked lazy, or the browser has not started fetching it yet. | Leave above-the-fold images eager, including likely LCP candidates. Check the page at realistic mobile and desktop sizes. |
| Cards jump or the grid changes height as images load. | No dimensions or aspect-ratio space was reserved. | Set image width and height, or reserve the intended ratio in the layout. |
| A lazy image never appears or has no visible space. | The image may have zero dimensions, an invalid source, or a CSS/layout issue. | Reserve nonzero space, inspect the image URL and network response, and check its computed display and dimensions. |
Code treats images as loaded at the window load event, but some are still pending. |
Lazy images may not have loaded when the window fires load. |
For a specific image, inspect its Boolean complete property and, if needed, handle that image’s load and error events. |
Adding loading to <picture> has no effect. |
The attribute belongs on the nested <img>. |
Place loading="lazy" on the child image element. |
| Lazy loading starts at a different distance than expected. | The fetch threshold is browser-controlled and can vary. | Treat the attribute as a hint. Use Intersection Observer only when you need a custom intersection-driven behavior. |
7. Performance, reliability, and cost considerations
- Initial rendering: Defer thumbnails below the fold to avoid requesting them immediately, while leaving important visible images eager.
- Layout stability: Provide dimensions or an aspect ratio so the browser can allocate space before image bytes arrive.
- Scheduling: Let the browser choose when to fetch native lazy images. Do not build logic that assumes a fixed pixel threshold.
- Completion checks: Do not use the window
loadevent as proof that every lazy image has finished. Check the specific image when necessary. - Implementation overhead: Native loading avoids a separate library for the basic case. Scripted visibility logic adds code and should be reserved for a concrete custom behavior.
- Cost: Lazy loading changes when the browser requests images; it does not by itself change the hosting or delivery price charged by your image provider.
8. Capture consistent directory thumbnails
If a directory includes previews of external websites, capturing each preview yourself requires browser or screenshot-service setup. ScreenshotNeo is a website screenshot API and MCP server. Its captures accept cookie or consent banners like a visitor and remove 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Only clean shots are billed: bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers.
Or skip the browser setup
Make one GET request for a website screenshot. See the ScreenshotNeo API documentation for the available options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write("shot.webp", res);
- Cookie banners, popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, and failed loads are never billed.
- An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account to get 1,000 screenshots per month with no card.
9. Frequently asked questions
Does loading="lazy" require JavaScript?
No. It is a native HTML image attribute for browser-controlled deferred loading.
Should I lazy-load every thumbnail in the directory?
No. Keep images likely to be visible in the initial viewport eager; apply lazy loading to images that begin farther down the page.
Can I choose the exact distance from the viewport that triggers loading?
Not with the native attribute. The browser controls that threshold. Use Intersection Observer when custom intersection behavior is required.
Does a lazy image count as loaded when the page fires load?
Not necessarily. A particular lazy image may still be pending; inspect that image’s complete property when your code needs to know its state.


