Skip to content

feat: use <picture> elements for responsive images @W-18714493#2724

Merged
seckardt merged 4 commits intodevelopfrom
fix/258-W-18960755-plp-lcp-regression
Jul 16, 2025
Merged

feat: use <picture> elements for responsive images @W-18714493#2724
seckardt merged 4 commits intodevelopfrom
fix/258-W-18960755-plp-lcp-regression

Conversation

@seckardt
Copy link
Contributor

@seckardt seckardt commented Jul 3, 2025

Description

This PR introduces using <picture> elements as the base for responsive images created by the <DynamicImage/> component. This change is needed to support art direction of images on our pages, but most importantly paves the way for using modern image formats like AVIF or WebP in our Dynamic Imaging Service (DIS), which is targeted for the next minor release already. The change also adds responsive preloading to the mix, which is otherwise tricky to get right.

This PR additionally adjusts the sizes of the responsive images on the PLP, because previously defined dimensions (see PR #2446) forgot to take the existence and the impact of the search refinements panel into account, which lead to a significant over-fetching of images (~400-500kB per page on e.g. a larger Mac desktop).

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (could cause existing functionality to not work as expected)
  • Other changes (non-breaking changes that does not fit any of the above)

Changes

  • (change1)

How to Test-Drive This PR

  • (step1)

Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes (not required for documentation updates)

Accessibility Compliance

You must check off all items in one of the follow two lists:

  • There are no changes to UI

or...

Localization

  • Changes include a UI text update in the Retail React App (which requires translation)

@seckardt seckardt requested a review from a team as a code owner July 3, 2025 09:00
@cc-prodsec
Copy link
Collaborator

cc-prodsec commented Jul 3, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

license/snyk check is complete. No issues have been found. (View Details)

@seckardt seckardt force-pushed the fix/258-W-18960755-plp-lcp-regression branch 5 times, most recently from 65eab27 to dfd8db9 Compare July 6, 2025 08:10
Copy link
Contributor

@joeluong-sfcc joeluong-sfcc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add details/demo on how to test this?

@seckardt seckardt force-pushed the fix/258-W-18960755-plp-lcp-regression branch from bc7239b to 0a86480 Compare July 8, 2025 19:22
@seckardt seckardt changed the title fix: address LCP regression on PLP caused by responsive image densities @W-18960755 fix: use <picture> elements for responsive images and support image densities @W-18714493 Jul 8, 2025
@seckardt seckardt force-pushed the fix/258-W-18960755-plp-lcp-regression branch 4 times, most recently from 2af870d to facbc52 Compare July 9, 2025 08:36
@seckardt seckardt force-pushed the fix/258-W-18960755-plp-lcp-regression branch 3 times, most recently from 0edf32c to 5315c47 Compare July 10, 2025 17:18
@seckardt seckardt dismissed bendvc’s stale review July 11, 2025 06:46

Irrelevant reasons. Also misses the point that we simply have to make that change to only to be able support modern image formats in one of the next two sprints.

@seckardt seckardt force-pushed the fix/258-W-18960755-plp-lcp-regression branch from 5315c47 to d7a3000 Compare July 11, 2025 06:56
@seckardt seckardt changed the title fix: use <picture> elements for responsive images and support image densities @W-18714493 feat: use <picture> elements for responsive images and support image densities @W-18714493 Jul 11, 2025
@seckardt seckardt force-pushed the fix/258-W-18960755-plp-lcp-regression branch from d7a3000 to b7ccb8d Compare July 11, 2025 07:17
joeluong-sfcc
joeluong-sfcc previously approved these changes Jul 11, 2025
@seckardt seckardt changed the title feat: use <picture> elements for responsive images and support image densities @W-18714493 feat: use <picture> elements for responsive images @W-18714493 Jul 14, 2025
@seckardt seckardt force-pushed the fix/258-W-18960755-plp-lcp-regression branch from d7e8992 to 5051b3a Compare July 14, 2025 16:38
() => getResponsiveImageAttributes({src, widths, breakpoints: theme.breakpoints}),
[src, widths, theme.breakpoints]
)
const [responsiveImageProps, numSources, effectiveImageProps, responsiveLinks] = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see this is memoized

Comment on lines +19 to +21
const vwValue = /^\d+vw$/
const pxValue = /^\d+px$/
const emValue = /^\d+em$/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a moment I was worried as I traversed a few function call depths that we weren't catching / throwing for values outside the pattern. Looks good 👍

Comment on lines +119 to +181
expect(elements[0]).toHaveAttribute('decoding', 'async')
expect(wrapper.firstElementChild).toBe(elements[0])
})

test('renders an image with explicit widths', () => {
const {getByTestId, getAllByTitle} = renderWithProviders(
<DynamicImage
data-testid={'dynamic-image'}
src={src}
imageProps={{
...imageProps,
loading: 'lazy'
}}
widths={['50vw', '50vw', '20vw', '20vw', '25vw']}
/>
)

const wrapper = getByTestId('dynamic-image')
const elements = getAllByTitle(imageProps.title)
expect(elements).toHaveLength(1)
expect(elements[0]).toHaveAttribute('src', src)
expect(elements[0]).toHaveAttribute('loading', 'lazy')
expect(elements[0]).toHaveAttribute('decoding', 'async')
expect(elements[0]).not.toHaveAttribute('sizes')
expect(elements[0]).not.toHaveAttribute('srcset')

expect(wrapper.firstElementChild).not.toBe(elements[0])
expect(wrapper.firstElementChild.tagName.toLowerCase()).toBe('picture')

const sourceElements = Array.from(wrapper.querySelectorAll('source'))
expect(sourceElements).toHaveLength(5)
expect(sourceElements[0]).toHaveAttribute('media', '(min-width: 80em)')
expect(sourceElements[0]).toHaveAttribute('sizes', '25vw')
expect(sourceElements[0]).toHaveAttribute(
'srcset',
[384, 768].map((width) => `${src} ${width}w`).join(', ')
)
expect(sourceElements[1]).toHaveAttribute('media', '(min-width: 62em)')
expect(sourceElements[1]).toHaveAttribute('sizes', '20vw')
expect(sourceElements[1]).toHaveAttribute(
'srcset',
[256, 512].map((width) => `${src} ${width}w`).join(', ')
)
expect(sourceElements[2]).toHaveAttribute('media', '(min-width: 48em)')
expect(sourceElements[2]).toHaveAttribute('sizes', '20vw')
expect(sourceElements[2]).toHaveAttribute(
'srcset',
[198, 396].map((width) => `${src} ${width}w`).join(', ')
)
expect(sourceElements[3]).toHaveAttribute('media', '(min-width: 30em)')
expect(sourceElements[3]).toHaveAttribute('sizes', '50vw')
expect(sourceElements[3]).toHaveAttribute(
'srcset',
[384, 768].map((width) => `${src} ${width}w`).join(', ')
)
expect(sourceElements[4]).not.toHaveAttribute('media')
expect(sourceElements[4]).toHaveAttribute('sizes', '50vw')
expect(sourceElements[4]).toHaveAttribute(
'srcset',
[240, 480].map((width) => `${src} ${width}w`).join(', ')
)

expect(Helmet.peek()?.linkTags ?? []).toStrictEqual([])
Copy link
Contributor

@bfeister bfeister Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not seeing an explicit test for the error thrown by invalid component usage, e.g.

 <DynamicImage
                    data-testid={'dynamic-image'}
                    src={src}
                    imageProps={{
                        ...imageProps,
                        loading: 'lazy'
                    }}
                    widths={['INVALID']}
                />

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, in that case it wasn't there before either. Didn't add/change anything to the widths logic/handling.

Comment on lines +657 to +659
// Each product image can take up the full 50% of the screen width
'50vw', // base <= 479px
'50vw', // sm >= 480px ; <= 767px
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does feel good to move away from the density logic that was here in the previous iteration 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure yet. The unconditional addition of images for double density and thus having no influence on the behavior at all is not necessarily the real thing either.

bfeister
bfeister previously approved these changes Jul 14, 2025
Copy link
Contributor

@bfeister bfeister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Very interested to hear the lab improvements yielded by this

joeluong-sfcc
joeluong-sfcc previously approved these changes Jul 15, 2025
Signed-off-by: Steffen Eckardt <3235219+seckardt@users.noreply.github.com>
@seckardt seckardt dismissed stale reviews from joeluong-sfcc and bfeister via c08463a July 16, 2025 18:02
@seckardt seckardt enabled auto-merge (squash) July 16, 2025 18:02
@seckardt seckardt merged commit 0c6d073 into develop Jul 16, 2025
36 checks passed
@seckardt seckardt deleted the fix/258-W-18960755-plp-lcp-regression branch July 17, 2025 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants