The Problem
Field data for a client's product detail pages showed CLS at 0.04 on Chrome, 0.05 on Firefox, and 0.31 on Safari. Mobile only. Lighthouse on a desktop with simulated mobile throttling reported 0.02. PageSpeed Insights lab data agreed. The lab tooling could not see the spike at all, but CrUX flagged the URLs as "Poor" because over a quarter of Safari mobile sessions were shifting hard enough to fail the threshold.
The pages render a hero gallery, three product cards, and a related-items strip. Every image used the modern CSS pattern — a wrapper with aspect-ratio set, the image filling it with width: 100%; height: 100%:
.product-thumb {
aspect-ratio: 4 / 3;
overflow: hidden;
}
.product-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
Clean, modern, works everywhere. Or it should have. On the affected Safari sessions, the wrapper was collapsing to zero height until the image loaded, then jumping to its natural 4:3 ratio. Every image triggered a layout shift the moment its bytes arrived. On a page with eight images above the fold, the cumulative score crossed 0.25.
The Web Vitals dashboard pinpointed it. Bad sessions clustered on iOS 17.2 through 17.4, and on macOS Safari 17.x. Newer iOS 18.x sessions were clean. Older iOS 16.x sessions were also clean. The regression was the middle band, and it traced back to a Safari bug in the 17.x line where aspect-ratio on a flex child silently falls back to auto.
Why It Happens
The CSS aspect-ratio property is supposed to reserve the box's intrinsic size before any contents arrive. The browser computes the height from the width using the declared ratio and lays out the surrounding content accordingly. When the image loads, it fills a box that is already the right size, and no shift occurs.
Safari 17 introduced a regression where aspect-ratio on a child of a flex or grid container with align-items: stretch was discarded during the initial layout pass. The browser used aspect-ratio: auto instead, which means "no reservation, wait for content." Content arrived, the box grew to fit it, and everything below shifted down. Apple fixed this in Safari 18, but a long tail of devices stays on the affected build because iOS minor updates do not always include Safari engine bumps.
The behaviour is documented in WebKit bug 270415. It is not a spec ambiguity — every other browser handles the same markup correctly. Production sites only see it because real users span more Safari builds than lab tooling does.
I ran into this on a client project right after we shipped a flexbox refactor of the product grid. The before-and-after Lighthouse comparison looked perfect. Field CrUX, two weeks later, told the actual story.
The Fix
Three layers, applied together. None of them alone covers all the affected sessions.
Layer 1: Set width and height HTML attributes on every image. The original sin of the modern aspect-ratio pattern is that it abandoned the HTML attributes that browsers have been using to reserve image space since the 1990s. Put them back. When the CSS fallback hits, the attributes still give the browser a ratio to reserve from:
<img
src="/products/thumb.webp"
width="800"
height="600"
alt="Product thumbnail"
loading="lazy"
/>
The attributes do not constrain the rendered size. With width: 100%; height: auto in CSS, the image still scales to its container. But before the bytes load, the browser uses the attribute ratio to reserve the right height. This works on every version of Safari that ever shipped.
Layer 2: Add an explicit min-height on the wrapper as a backstop. Even with attributes set, the affected Safari builds can still collapse a flex child whose only height contribution is intrinsic. A min-height tied to the expected aspect ratio holds the layout:
.product-thumb {
aspect-ratio: 4 / 3;
overflow: hidden;
min-height: 0; /* allow shrinking on small viewports */
}
@supports not (aspect-ratio: 4 / 3) {
.product-thumb {
padding-top: 75%; /* 4:3 fallback */
position: relative;
}
.product-thumb img {
position: absolute;
inset: 0;
}
}
The @supports not block covers older browsers that never shipped aspect-ratio at all. The min-height: 0 cleans up a separate flex sizing quirk where children refuse to shrink below their content size.
Layer 3: Avoid align-items: stretch on the offending containers. If the parent flex container does not need stretch, set it explicitly to flex-start or use grid with explicit row tracks. The Safari regression only triggers when the child is being stretched by its parent. Removing the trigger removes the bug:
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-auto-rows: max-content; /* explicit row sizing, not stretch */
gap: 1.5rem;
}
For Next.js Image components, the fix is mostly already there. next/image sets width and height attributes when you pass numeric props. The trap is that fill mode drops them in favour of position: absolute, which interacts badly with the Safari bug. If you have CLS issues on Safari with fill images, switch to explicit sizes:
<Image
src="/products/thumb.webp"
width={800}
height={600}
alt="Product thumbnail"
sizes="(max-width: 768px) 100vw, 33vw"
style={{ width: '100%', height: 'auto' }}
/>
Verify with real-device testing. Lighthouse will not catch this — it runs on a stable Chromium build. Use BrowserStack on an iOS 17.3 device, or BrowserStack's Web Vitals capture, or wait 72 hours and watch the CrUX dashboard. Field CLS should drop to the 0.05 baseline within two days of deploy.
If you cannot wait for CrUX to update, sample real users with the web-vitals library and segment by user agent. A 24-hour sample is usually enough to confirm the Safari band has come down.
The Lesson
Modern CSS is not a free pass on the old HTML hygiene. Set width and height attributes on every image, add a min-height backstop for flex children, and audit your grid containers for stretch alignment that does not need to be there. The bug is fixed in Safari 18, but the affected device population will be in CrUX data for another year.
If your Core Web Vitals dashboard has a Safari-only regression you cannot reproduce in lab tooling, that is a project I get paid to debug. See my services. For a related CLS hunt from the same toolkit, read CLS skeleton loaders Suspense fallback.
Safari-only Core Web Vitals regressions eating your rankings? Get it diagnosed.