The Images Weren't Broken, My Way of Checking Them Was: Doubting the iframe First, Then Reading currentSrc
A record of misdiagnosing healthy images as unloaded because I audited them through an iframe in a separate tab. In a hidden frame, naturalWidth reads 0 at a rendered width of 0, and lazy images never start. Here are the numbers I reproduced locally, an audit script that reads currentSrc and rendered width on the real page, and the trap of bulk-replacing CSS values with a regex.
I was auditing the images on my own site one evening. What I wanted to know was simple: do the images on every page actually show up, on both a phone-width and a desktop-width screen?
Opening pages one by one was tedious, so I built a throwaway audit tab that embedded the live pages in an iframe, switched the width between 375px and 1280px, and read each image's state. The result listed several images as "not loaded."
I panicked and started thinking about what to fix. Then I opened the real pages in a normal tab, and every image was there. The images were fine. My way of checking was what had broken.
Below I walk through what was happening, using numbers I reproduced and measured on my own machine, and the order in which I now doubt things when an image looks wrong.
The first thing to say is the order of suspicion
When an image looked missing, I used to suspect the URL or the server first. These days I reverse the order.
Is the place I am checking in a state where the image is even allowed to start loading?
Am I reading the state after loading has started?
Only if it is still empty after both, suspect the image itself.
Most misdiagnoses happen at steps one and two. The image was never broken. I was measuring in a place where loading cannot start, or at a moment before it starts.
The setup, and the results up front
I used Playwright with headless Chromium on my own machine. The test page was tiny: two images displayed at 400px wide and one pointing to a file that does not exist. The first image had loading="lazy" and sat about 6,000px down the page. The second was an ordinary image with srcset and sizes.
Where I checked
Lazy image
Normal image (srcset)
Real page opened directly (1280px wide)
Before scrolling: complete=false, empty currentSrc. After scrolling: loaded
currentSrc picked the 800w candidate; naturalWidth was 800
Visible iframe (375px wide)
complete=false, currentSrc stayed empty
currentSrc picked the 400w candidate; naturalWidth was 374
iframe with display:none
complete=false, rendered width 0
complete=true yet naturalWidth was 0, and rendered width was 0
This table explains most of my misdiagnosis. In a hidden frame, even a fully loaded ordinary image reports naturalWidth as 0, while complete is true. A check written as "naturalWidth of 0 means broken" falls apart right there.
I noticed one more thing about naturalWidth. In a 375px frame, srcset picked the 400px-wide file, whose real size is 400px, yet naturalWidth was 374. With width descriptors, naturalWidth is not the raw pixel count but a value divided by the density the browser selected. A mismatch with the file's real size does not mean the image is being shrunk.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Before calling an image broken, you can read rendered width, load state and currentSrc to tell apart a truly broken image, one that has not started loading, and one your test frame is simply unable to measure
✦Before asking an agent to confirm that images display, you can write where to check and what counts as a pass into the instruction itself, saving the time you would spend fixing things a false report said were broken
✦Before bulk-replacing CSS numbers, you can run a check that catches media-query breakpoints being shrunk along with everything else, preventing a quiet layout drift that is painful to trace afterwards
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
If you want to know which image the browser chose and how wide it actually drew it, three values are enough.
currentSrc: the URL the browser actually picked. It changes with width. If it is empty, nothing has been picked yet.
getBoundingClientRect().width: the drawn width. If it is 0, nothing is being drawn at all.
the pair complete and naturalWidth: loading finished but the content is empty means the image is truly broken.
The first snippet is the smallest reader. Paste it into the browser console on the real page and run it.
// Run in the console of the real page: list each image's stateconsole.table( [...document.images].map((img) => { const r = img.getBoundingClientRect(); return { src: img.getAttribute("src"), lazy: img.loading === "lazy", complete: img.complete, naturalWidth: img.naturalWidth, currentSrc: img.currentSrc.split("/").pop() || "(not chosen)", renderedWidth: Math.round(r.width), }; }));
When I wanted to compare several widths, I did not keep pasting that. I folded it into the audit script below, which is where the effort really changes.
The audit script: classify, scroll, classify again
The second snippet is the Playwright script I rewrote for audits. It has three points: images with a rendered width of 0 are set aside as "cannot be measured", images whose lazy loading has not started are not mixed in with broken ones, and the script always scrolls through the page and reads again at the end.
# verify_images.py — classify image state on the real page (Playwright / Python 3)import sysfrom playwright.sync_api import sync_playwrightSNAPSHOT = """() => [...document.images].map(i => { const r = i.getBoundingClientRect(); return { id: i.id || i.getAttribute('src'), complete: i.complete, nw: i.naturalWidth, cur: i.currentSrc.split('/').pop(), rw: Math.round(r.width), lazy: i.loading === 'lazy' };})"""def classify(s): if s["rw"] == 0: return "hidden" # rendered width 0: do not trust naturalWidth here if s["lazy"] and s["cur"] == "" and not s["complete"]: return "not-started" # lazy loading has not kicked in yet if s["complete"] and s["nw"] == 0: return "BROKEN" # finished loading but empty return "ok"def scroll_through(page, step=600): h = page.evaluate("document.documentElement.scrollHeight") for y in range(0, h + step, step): page.evaluate(f"window.scrollTo(0, {y})") page.wait_for_timeout(120) # give lazy loading time to fire page.wait_for_timeout(500)def check(url, width): with sync_playwright() as p: b = p.chromium.launch() page = b.new_page(viewport={"width": width, "height": 800}) page.goto(url, wait_until="load") first = {s["id"]: classify(s) for s in page.evaluate(SNAPSHOT)} scroll_through(page) after = page.evaluate(SNAPSHOT) print(f"== {url} @ {width}px") for s in after: print(f'{s["id"]:12} first={first[s["id"]]:12} ' f'after={classify(s):8} cur={s["cur"] or "-":10} ' f'nw={s["nw"]:5} rw={s["rw"]}') b.close()if __name__ == "__main__": check(sys.argv[1], int(sys.argv[2]))
Here is the output against my reproduction page at 375px and 1280px.
below, the lazy image, started as not-started and became ok after scrolling. If I had measured only once at the beginning and reported it as broken, I would have repeated my original mistake. gone points to a file that does not exist, so it stays BROKEN both before and after. A truly broken image is the one that does not change with time.
Notice also that cur for eager is img_s.png at 375px and img_m.png at 1280px. Which file gets chosen depends on width, which is exactly why checking at more than one width pays off.
If you must measure inside a frame, match the conditions
I do not think iframe auditing should be abandoned entirely. It is handy when you want to skim many pages quickly. But the numbers cannot be trusted unless these conditions hold.
The frame is placed at a visible size, not display:none or off-screen.
At the end of the audit, you scroll the window inside the frame so that lazy loading fires.
The frame width matches the device width you care about, since a different width yields a different currentSrc.
You judge by rendered width and currentSrc together, not by naturalWidth alone.
Even so, a frame's result can drift from what the real page shows. My rule now is that anything suspicious in the frame is re-checked on the real page, and I only fix images whose symptom reproduces there.
An agent inherits the same misdiagnosis
None of this is limited to checking by hand. When you ask an Antigravity agent to "confirm every image displays," the same trap applies. Unless the instruction says where to check and what counts as a pass, it is entirely possible for the agent to read only naturalWidth inside a frame and report "broken."
Here is the skeleton of the instruction I now write down.
Please confirm that images display. Follow these conditions.1. Open the real page directly. Do not judge from an audit page that embeds it in an iframe.2. Check at both 375px and 1280px wide.3. Do not decide by naturalWidth alone. Also record the rendered width (it must not be 0) and currentSrc.4. After the first read, scroll to the bottom of the page, read again, and report what changed.5. For any image judged broken, reopen the real page and confirm the symptom reproduces. Report only those that do.
When I read the report, I look for whether it says "only those that reproduce." If that line is missing, I set the report aside for the moment. Putting one reproduction between the report and the fix was enough to save me the time of fixing things that were never broken.
The second failure: a mechanical replacement shrank the breakpoints too
During the same audit I made another mistake. I wanted to make displayed image widths smaller across the board, so I used a regex to rewrite every px value in the CSS at once.
Part of the layout drifted without any error. The media-query thresholds had been shrunk along with everything else. Here is a three-line reproduction of what happened.
# shrink.py — halve the numbers: a naive version and a safe version (Python 3)import recss = """.gallery img { width: 800px; max-height: 600px; }@media (min-width: 768px) { .gallery img { width: 640px; } }@media (max-width: 480px) { .gallery img { width: 320px; } }"""def naive(css, ratio): # shrinks every "<number>px", including @media thresholds return re.sub(r"(\d+)px", lambda m: f"{round(int(m.group(1))*ratio)}px", css)PROP = r"(?<![\w-])(width|max-height)(\s*:\s*)(\d+)px" # excludes min-width / max-widthdef scale_props(text, ratio): return re.sub(PROP, lambda k: f"{k.group(1)}{k.group(2)}{round(int(k.group(3))*ratio)}px", text)def safe(css, ratio): out = [] for line in css.splitlines(): m = re.match(r"\s*@media[^{]*\{(.*)\}\s*$", line) if m: # leave the parenthesised condition alone; scale only inside the braces head = line[: line.index("{") + 1] out.append(head + scale_props(line[len(head):], ratio)) else: out.append(scale_props(line, ratio)) return "\n".join(out) + "\n"print("--- naive 0.5 ---"); print(naive(css, 0.5))print("--- safe 0.5 ---"); print(safe(css, 0.5))
In the naive version, min-width: 768px became 384px and max-width: 480px became 240px. The screen widths at which the layout switches were shrunk right along with the images. The boundary between tablet and phone moves, and the result is the hardest kind of drift to trace: every width looks slightly off.
The safe version targets only the property names width and max-height, and shrinks just their values. It skips min-width and max-width through the leading hyphen check, and never touches the parenthesised condition of an @media. Still, this is a very small version built for three lines of CSS. Real stylesheets mix variables, calc() and @supports, and regular expressions have limits. Once the replacement grows beyond a few rules, handing it to a parser that understands CSS is the sounder decision.
The traps, as a table to check before and after
What is easy to do
What happens
What to confirm first
Audit images in a hidden iframe
Rendered width is 0, so naturalWidth reads 0 and loaded images look broken
Is the frame placed at a visible size?
Measure once without scrolling
Lazy loading has not started, so complete stays false and currentSrc stays empty
Did you read again after scrolling to the bottom?
Compare naturalWidth to the real file size
With srcset width descriptors it is divided by the chosen density, so it will not match
Did you compare the currentSrc file instead?
Bulk-replace numbers with a regex
Media-query thresholds shrink too, and the breakpoints drift
Does the diff contain any @media lines?
What to try next
If you are auditing images through an iframe today, I would be grateful if you ran the script once on the real page at the end. Any image where the frame and the real page disagree is a clue to the problem hiding in how you check.
For my own audits, I added one line at the very start of the procedure: before calling an image broken, stop and ask whether the place I am checking lets it load at all. Putting a reproduction before every fix is a line I intend to keep holding.
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.