Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Picture/Image Optimisations - The DPR Problem (Part 1/?) #3641

Merged
merged 22 commits into from
Nov 29, 2021

Conversation

OllysCoding
Copy link
Contributor

@OllysCoding OllysCoding commented Nov 15, 2021

Summary

On DCR, there is a bug with how images are rendered which means high resolution displays can often fetch excessively-large variants of images. This means increased cost in bandwidth for both our users and our fastly bill.

This PR replicates the functionality of this Frontend PR to make use per-breakpoint source elements with media attributes to prevent the browser from selecting larger image sources than we intend.

Some related & interesting background PR's & Articles:

  1. This PR's Frontend Equivalent
  2. Whatwg html spec discussion (And the comment which mentions our solution!)
  3. Twitter engineering blog post about a similar change they made!

Background Info

Documentation on how picture tags work & more - A great place to start if this is new to you! (Added in this PR)

What does DCR currently do for images?

DCR renders images in <picture> tags, with each picture element containing two <source> elements. These source elements differentiate between high & low DPR/DPI (device pixel ratios).

We use the following media query on the first of our two <source> elements to tell the browser, "Hey, if you're a 'high DPR device', use this source element":
<source media"(-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi)" >

This lets us do something different for these "High DPR" devices, often on these super high resolution screens, getting an equivalently high resolution image is unnecessary - the difference is barely perceptible to the human eye.

For non-high-dpr image sources we provide the quality attribute to fastly as ?quality=85, however for these high-dpr image sources we have, we tell fastly ?quality=45&dpr=2.

These high-dpr image sources have, as we learned earlier, double the resolution of the regular image sources, however by providing a lower quality number, they're more compressed. At some point in time someone at The Guardian sat and looked at images side-by-side and decided this config was the best for high resolution devices.

All in all this leads to a DCR picture element looking something like:

<picture>
    <source media="(-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi)" sizes="(min-width: 660px) 620px, 100vw" srcset="https://fastly.link/path?width=300&quality=45&dpr=2 600w, https://fastly.link/path?width=600&quality=45&dpr=2 1200w" >
    <source sizes="(min-width: 660px) 620px, 100vw" srcset="https://fastly.link/path?width=300&quality=85 300w, https://fastly.link/path?width=600&quality=85 600w" >
    <img src="https://fastly.link/to/fallback/for/unsupported/browsers" > 
</picture>

The DPR Problem

Everything up until now is context & background information to make sure anyone who reads this PR can understand what the changes made actually do. Now let's talk about the problem, why it happens & how we have solved it!

This problem comes from how the browser tries to compensate for high DPR displays when choosing an image source. As described above, we provide a set of sources for high DPR displays, and target them with a media query to ensure it's picked.

Unfortunately the browser itself tries to compensate for high DPR as well, but in a less efficient way. Once the browser has figured out what size source it wants from the sizes attribute on our source element, it's then multiplied by the DPR of the display to get the desired width it will look for in our srcset:

# desiredWidth is the width of an image that the browser will look for in `srcset`
# size is the chosen size based off our queries in the `sizes` attribute
# DPR is the ration between CSS Pixels & device resolution (e.g 2, 3, 4)
desiredWidth = size * DPR

This poses a problem for us, because if we tell the browser "Hey choose a 620px image", and the browser has a DPR of say 3, it will actually look for an image source for 1860px, far higher resolution than is needed for the user to have a good experience.

How do we solve this?

Lucky for us, this problem has already been solved on Frontend, and its existence is currently a regression on DCR.

So what does Frontend do? As shown in this PR, rather than having just 2 source elements, we instead have 2 source elements per breakpoint (one for high DPR, one for regular displays), and provide just one size & source set for that source element.

Lets look at a simplified example (with only 1 source per breakpoint):

<picture>
    <source media="min-width: 980px" srcset="https://xxx.png?width=620px 620w" />
    <source media="min-width: 660px" srcset="https://xxx.png?width=620px 620w" />
    <source media="min-width: 480px" srcset="https://xxx.png?width=480px 480w" />
    <source media="min-width: 375px" srcset="https://xxx.png?width=420px 420w" />
</picture>

In this example, we have the logic that usually would have been in our sizes attribute (sizes="(min-width: 660px) 620px, 100vw") extrapolated into individual source elements. All our breakpoint which are 660px or larger offer only 1 choice, the 620px source. The lower breakpoints have looked for sources which are closest to their own size, as a replacement for 100vw.

This solves our DPR problem because, the media attribute uses CSS pixels, and because we only offer 1 image source for each of these elements, once the browser has picked its source element, we basically strong arm it into which source to use. Even if it multiplies the size it gets from sizes, it still has to use the source we give as it's the only available option.

Implementation details

The primary change for this PR is the (previously called) getSizes function. This function takes various information about the image we're trying to render (role, layout, etc), and returns the contents for the sizes attribute. By refactoring this code, we can instead have it also accept a breakpoint, and have it perform the same logic as the queries it was producing to instead return an expected image width for that breakpoint:

// Before
getSizes(...) -> "(min-width: 660px) 620px, 100vw"
// after
getDesiredWidthForBreakpoint(660, ...) -> 620
getDesiredWidthForBreakpoint(480, ...) -> 480

I've also introduced an optimisation function called optimiseBreakpointSizes which will look for redundant source's and remove them (for example if all your sources for breakpoint 660px and above return 620px, you only need 1). This should help avoid us overfilling the DOM with source elements.

Final before and after

Before:

<picture itemprop="contentUrl">
    <source srcset="https://i.guim.co.uk/img/media/file.jpg?width=620&amp;quality=45&amp;auto=format&amp;fit=max&amp;dpr=2&amp;s=xxx 1240w,https://i.guim.co.uk/img/media/file.jpg?width=700&amp;quality=45&amp;auto=format&amp;fit=max&amp;dpr=2&amp;s=xxx 1400w,https://i.guim.co.uk/img/media/file.jpg?width=620&amp;quality=45&amp;auto=format&amp;fit=max&amp;dpr=2&amp;s=xxx 1240w,https://i.guim.co.uk/img/media/file.jpg?width=645&amp;quality=45&amp;auto=format&amp;fit=max&amp;dpr=2&amp;s=xxx 1290w,https://i.guim.co.uk/img/media/file.jpg?width=465&amp;quality=45&amp;auto=format&amp;fit=max&amp;dpr=2&amp;s=xxx 930w" sizes="(min-width: 660px) 620px, 100vw" media="(-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/file.jpg?width=620&amp;quality=85&amp;auto=format&amp;fit=max&amp;s=xxx 620w,https://i.guim.co.uk/img/media/file.jpg?width=700&amp;quality=85&amp;auto=format&amp;fit=max&amp;s=xxx 700w,https://i.guim.co.uk/img/media/file.jpg?width=620&amp;quality=85&amp;auto=format&amp;fit=max&amp;s=xxx 620w,https://i.guim.co.uk/img/media/file.jpg?width=645&amp;quality=85&amp;auto=format&amp;fit=max&amp;s=xxx 645w,https://i.guim.co.uk/img/media/file.jpg?width=465&amp;quality=85&amp;auto=format&amp;fit=max&amp;s=xxx 465w" sizes="(min-width: 660px) 620px, 100vw">
    <img alt="The Palace Theatre, London, showing Harry Potter and the Cursed Child" src="https://i.guim.co.uk/img/media/file.jpg?width=465&amp;quality=45&amp;auto=format&amp;fit=max&amp;dpr=2&amp;s=xxx" height="1200" width="2000" class="dcr-1989ovb">
</picture>

After:

<picture>
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=620&quality=45&auto=format&fit=max&dpr=2& 1240w" media="(min-width: 980px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: 980px) and (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=620&quality=85&auto=format&fit=max& 620w" media="(min-width: 980px)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=700&quality=45&auto=format&fit=max&dpr=2& 1400w" media="(min-width: 740px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: 740px) and (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=700&quality=85&auto=format&fit=max& 700w" media="(min-width: 740px)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=620&quality=45&auto=format&fit=max&dpr=2& 1240w" media="(min-width: 660px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: 660px) and (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=620&quality=85&auto=format&fit=max& 620w" media="(min-width: 660px)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=620&quality=45&auto=format&fit=max&dpr=2& 1240w" media="(min-width: 480px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: 480px) and (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=620&quality=85&auto=format&fit=max& 620w" media="(min-width: 480px)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=465&quality=45&auto=format&fit=max&dpr=2& 930w" media="(min-width: 375px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: 375px) and (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=465&quality=85&auto=format&fit=max& 465w" media="(min-width: 375px)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=465&quality=45&auto=format&fit=max&dpr=2& 930w" media="(min-width: 320px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: 320px) and (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=465&quality=85&auto=format&fit=max& 465w" media="(min-width: 320px)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=465&quality=45&auto=format&fit=max&dpr=2& 930w" media="(min-width: 0px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: 0px) and (min-resolution: 120dpi)">
    <source srcset="https://i.guim.co.uk/img/media/picture.jpg?width=465&quality=85&auto=format&fit=max& 465w" media="(min-width: 0px)">
    <img alt="The Palace Theatre, London, showing Harry Potter and the Cursed Child" src="https://i.guim.co.uk/img/media/picture.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=0492ab78e73c5167d8b4b841e601fbd4" height="1200" width="2000" class="dcr-b5pnrc-css">
</picture>

Future Improvements

There are a few outstanding issues that can be solved with this solution,

  1. More effective immersive main media images by looping through sources individually rather than matching sources to breakpoints
  2. Solve the DPR issue for immersive main media portrait as well (This also remains an issue in Frontend)
  3. Investigate if it's more effective to generate sources within DCR rather than in Frontend

Once these PR's are raised they'll be linked to here.

@github-actions
Copy link

github-actions bot commented Nov 15, 2021

Size Change: +793 B (0%)

Total Size: 3.12 MB

Filename Size Change
dotcom-rendering/dist/frontend.server.js 2.49 MB +132 B (0%)
dotcom-rendering/dist/OnwardsLower.js 9.67 kB +11 B (0%)
dotcom-rendering/dist/OnwardsLower.legacy.js 9.89 kB +21 B (0%)
dotcom-rendering/dist/OnwardsUpper.js 14.1 kB +88 B (+1%)
dotcom-rendering/dist/OnwardsUpper.legacy.js 14.4 kB +92 B (+1%)
dotcom-rendering/dist/react.js 138 kB +218 B (0%)
dotcom-rendering/dist/react.legacy.js 143 kB +231 B (0%)
ℹ️ View Unchanged
Filename Size
dotcom-rendering/dist/101.js 21.1 kB
dotcom-rendering/dist/101.legacy.js 21.1 kB
dotcom-rendering/dist/316.js 2.81 kB
dotcom-rendering/dist/316.legacy.js 2.94 kB
dotcom-rendering/dist/atomIframe.js 1.87 kB
dotcom-rendering/dist/atomIframe.legacy.js 2.13 kB
dotcom-rendering/dist/braze-web-sdk-core.js 36.1 kB
dotcom-rendering/dist/braze-web-sdk-core.legacy.js 36.1 kB
dotcom-rendering/dist/coreVitals.js 4.03 kB
dotcom-rendering/dist/coreVitals.legacy.js 4.31 kB
dotcom-rendering/dist/dynamicImport.js 2.99 kB
dotcom-rendering/dist/dynamicImport.legacy.js 3.27 kB
dotcom-rendering/dist/EditionDropdown.js 693 B
dotcom-rendering/dist/EditionDropdown.legacy.js 701 B
dotcom-rendering/dist/elements-CalloutBlockComponent.js 5.92 kB
dotcom-rendering/dist/elements-CalloutBlockComponent.legacy.js 6.27 kB
dotcom-rendering/dist/elements-DocumentBlockComponent.js 571 B
dotcom-rendering/dist/elements-DocumentBlockComponent.legacy.js 602 B
dotcom-rendering/dist/elements-InstagramBlockComponent.js 434 B
dotcom-rendering/dist/elements-InstagramBlockComponent.legacy.js 452 B
dotcom-rendering/dist/elements-InteractiveBlockComponent.js 2.94 kB
dotcom-rendering/dist/elements-InteractiveBlockComponent.legacy.js 3.08 kB
dotcom-rendering/dist/elements-InteractiveContentsBlockComponent.js 1.9 kB
dotcom-rendering/dist/elements-InteractiveContentsBlockComponent.legacy.js 1.98 kB
dotcom-rendering/dist/elements-MapEmbedBlockComponent.js 1.86 kB
dotcom-rendering/dist/elements-MapEmbedBlockComponent.legacy.js 1.92 kB
dotcom-rendering/dist/elements-RichLinkComponent.js 3.27 kB
dotcom-rendering/dist/elements-RichLinkComponent.legacy.js 3.31 kB
dotcom-rendering/dist/elements-SpotifyBlockComponent.js 1.78 kB
dotcom-rendering/dist/elements-SpotifyBlockComponent.legacy.js 1.84 kB
dotcom-rendering/dist/elements-VideoFacebookBlockComponent.js 1.86 kB
dotcom-rendering/dist/elements-VideoFacebookBlockComponent.legacy.js 1.93 kB
dotcom-rendering/dist/elements-VineBlockComponent.js 579 B
dotcom-rendering/dist/elements-VineBlockComponent.legacy.js 594 B
dotcom-rendering/dist/elements-YoutubeBlockComponent.js 2.41 kB
dotcom-rendering/dist/elements-YoutubeBlockComponent.legacy.js 2.51 kB
dotcom-rendering/dist/embedIframe.js 1.88 kB
dotcom-rendering/dist/embedIframe.legacy.js 2.13 kB
dotcom-rendering/dist/ga.js 3.88 kB
dotcom-rendering/dist/ga.legacy.js 4.13 kB
dotcom-rendering/dist/GetMatchStats.js 3.31 kB
dotcom-rendering/dist/GetMatchStats.legacy.js 3.39 kB
dotcom-rendering/dist/guardian-braze-components-banner.js 9.81 kB
dotcom-rendering/dist/guardian-braze-components-banner.legacy.js 9.82 kB
dotcom-rendering/dist/guardian-braze-components-end-of-article.js 6.6 kB
dotcom-rendering/dist/guardian-braze-components-end-of-article.legacy.js 6.61 kB
dotcom-rendering/dist/MostViewedFooterData.js 6.32 kB
dotcom-rendering/dist/MostViewedFooterData.legacy.js 6.41 kB
dotcom-rendering/dist/MostViewedRightWrapper.js 3.93 kB
dotcom-rendering/dist/MostViewedRightWrapper.legacy.js 4.1 kB
dotcom-rendering/dist/newsletterEmbedIframe.js 1.83 kB
dotcom-rendering/dist/newsletterEmbedIframe.legacy.js 2.08 kB
dotcom-rendering/dist/ophan.js 7.17 kB
dotcom-rendering/dist/ophan.legacy.js 7.36 kB
dotcom-rendering/dist/sentry.js 677 B
dotcom-rendering/dist/sentry.legacy.js 687 B
dotcom-rendering/dist/sentryLoader.js 4.72 kB
dotcom-rendering/dist/sentryLoader.legacy.js 7.67 kB
dotcom-rendering/dist/shimport.js 2.75 kB
dotcom-rendering/dist/shimport.legacy.js 2.76 kB
dotcom-rendering/dist/SignInGateMain.js 1.84 kB
dotcom-rendering/dist/SignInGateMain.legacy.js 1.87 kB

compressed-size-action

desiredWidth: number,
inlineSrcSets: SrcSetItem[],
): SrcSetItem => {
// For a desired width, find the SrcSetItem which is the closest match
const sorted = inlineSrcSets.sort((a, b) => b.width - a.width);
// A match greated than the desired width will always be picked when one is available
const sorted = inlineSrcSets.slice().sort((a, b) => b.width - a.width);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding .slice() to avoid sideeffects as .sort modifies the original array

Copy link
Contributor

Choose a reason for hiding this comment

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

such awareness of the evil of mutation...

Comment on lines +109 to +113
if (
breakpoint >= breakpoints.tablet &&
breakpoint < breakpoints.desktop
)
return 680;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking at DCR, there is a breakpoint where the image width increases to aprx. 680px. This is brings DCR in parity with Frontend, which honoured this.

return `(min-width: ${breakpoints.wide}px) 380px, 300px`;
if (breakpoint >= breakpoints.wide) return 380;
if (breakpoint >= breakpoints.tablet) return 300;
return breakpoint; // 100vw
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once under the tablet breakpoint, this is 100vw instead of 300px as it was before

@OllysCoding OllysCoding marked this pull request as ready for review November 16, 2021 16:16
@OllysCoding OllysCoding changed the title Picture/Image Optimisations (Part 1/?) Picture/Image Optimisations - The DPR Problem (Part 1/?) Nov 16, 2021
@oliverlloyd oliverlloyd requested review from a team and JamieB-gu November 16, 2021 16:53
desiredWidth: number,
inlineSrcSets: SrcSetItem[],
): SrcSetItem => {
// For a desired width, find the SrcSetItem which is the closest match
const sorted = inlineSrcSets.sort((a, b) => b.width - a.width);
// A match greated than the desired width will always be picked when one is available
const sorted = inlineSrcSets.slice().sort((a, b) => b.width - a.width);
Copy link
Contributor

Choose a reason for hiding this comment

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

such awareness of the evil of mutation...

dotcom-rendering/src/web/components/Picture.tsx Outdated Show resolved Hide resolved
@mchv
Copy link
Member

mchv commented Nov 17, 2021

Excellent work writing that PR @OllysCoding 💯 🏅

These high-dpr image sources have, as we learned earlier, double the resolution of the regular image sources, however by providing a lower quality number, they're more compressed. At some point in time someone at The Guardian sat and looked at images side-by-side and decided this config was the best for high resolution devices.

Unless I am wrong that person is @paperboyo and when he is back may be able to provide some meaningful comments.

The code use 'number' to describe both 'pixel' and 'viewport widths'.
This add `Pixel` type alias to make the return type explicit and ease code reading.
@mchv
Copy link
Member

mchv commented Nov 17, 2021

@OllysCoding I have appended a commit adding a type alias for Pixel as I was unclear what unit return getSizeForBreakpoint, but the way it used at the end of the function seems to always been px.

Copy link
Contributor

@oliverlloyd oliverlloyd left a comment

Choose a reason for hiding this comment

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

Awesome stuff!

Comments are more for my own notes as we're pairing on this

dotcom-rendering/src/web/components/Picture.tsx Outdated Show resolved Hide resolved
dotcom-rendering/src/web/components/Picture.tsx Outdated Show resolved Hide resolved
dotcom-rendering/src/web/components/Picture.tsx Outdated Show resolved Hide resolved
dotcom-rendering/src/web/components/Picture.tsx Outdated Show resolved Hide resolved
dotcom-rendering/src/web/components/Picture.tsx Outdated Show resolved Hide resolved
@OllysCoding
Copy link
Contributor Author

Thanks for your feedback @mchv! Myself and @oliverlloyd went back over the code to work on readability & maintainability, which I hope is now much better (there were far too many sizes, widths, sources and srcsets all over the place!).

@JamieB-gu
Copy link
Contributor

Excellent job in the PR description, very clear summary of a complex problem 😬!

Brief question - I don't claim to know the answer but I'm curious what you think. Currently I believe you're proposing to have a <source> tag for each breakpoint and each supported DPR? So number of <source> tags, s, is number of breakpoints supported, b, multiplied by number of DPRs supported, d:

s = b x d

In our case we have 8 breakpoints and 2 supported DPRs, so number of <source> tags is 16.

What do you think of the second option proposed in guardian/image-rendering#239? In that scenario we provide a <source> tag per breakpoint, and use srcset to manage the DPR variations. That would reduce the number of <source> tags and drop the need to worry about the webkit (Safari) version of min-resolution as @mxdvl mentioned.

Again, I don't know if that's better, as you can see in that issue it's only one of two possible solutions (the other being the approach taken in this PR). But given that you've done a lot of research on this recently I'd be curious what you think 🙂.

Also I'm looking forward to getting all the excellent work in this PR available in common-rendering so AR can benefit too! 🎉

@paperboyo
Copy link
Contributor

Excellent job in the PR description, very clear summary of a complex problem 😬!

Can’t agree enough! Great job, @OllysCoding!

I’m very interested in opinions on different approaches, as Jamie says, but don’t have an opinion myself. I didn’t know our frontend logic uses media queries not supported by Safari! I would only say that I think it’s beneficial for the URLs to explicitly spell out dpr argument instead of us just changing width. We may introduce more steps in the future (or only for some, eg. immersive/fluid, imagery) and I think this will help then and anyhow seeing dpr in URL is just clearer.

@OllysCoding
Copy link
Contributor Author

Thanks for the feedback @JamieB-gu!

In our case we have 8 breakpoints and 2 supported DPRs, so number of tags is 16.

I was aware while writing this code that it would result in a lot more elements, which is why I added removeRedundantWidths (a feature the frontend implementation does not have), to weed out any source tags which aren't needed (e.g, if all image widths > desktop breakpoint are 620px wide for example, then we don't need the elements for the larger breakpoints).

Worth noting that removeRedundantWidths would work with either the current implementation, or the one suggested in guardian/image-rendering#239

What do you think of the second option proposed in guardian/image-rendering#239?

I think ultimately the differences between the two options are very slight.

The key tradeoff between the two is really, how declarative do you want to be at the cost of a larger DOM? The former (as implemented in this PR) gives us the choice of when to switch between high and low DPR variants, which could be beneficial if we want to look into if our choice of 1.25 is the most optimal. The second option entrusts the browser with this responsibility, and if there is room for optimisation there, we lose it - but we do get a slightly smaller DOM structure.

For me it makes sense to stick with the implementation which we use on Frontend, since it's known to work well, and I don't think there's a huge cost to the safari workaround we use.

I do also want to add further optimisations to this solution in future PRs, so I'd be interested to revisit the two solutions once the full context of the work is complete, and see if there's a more obvious choice!

@paperboyo
Copy link
Contributor

gives us the choice of when to switch between high and low DPR variants, which could be beneficial if we want to look into if our choice of 1.25 is the most optimal. The second option entrusts the browser with this responsibility

Oh, I do have an opinion now, then :-). I’m in favour of switching at 1.25, as this means my usually-zoomed-in-to-133% desktop browser gets HiDPI images as is always have, haha (in all seriousness, whether what I like is most optimal indeed, I don’t know). We also have slightly different cut-off point for cutouts, but this was purely because PNGs were too weighty which is now moot point as everyone gets WebP (and, hopefully, soonish – AVIF/JXL).

Copy link
Contributor

@mxdvl mxdvl left a comment

Choose a reason for hiding this comment

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

It’s great to see people caring about images 🖼️ 🎉

What do you think of the second option proposed in guardian/image-rendering#239? In that scenario we provide a <source> tag per breakpoint, and use srcset to manage the DPR variations. That would reduce the number of <source> tags and drop the need to worry about the webkit (Safari) version of min-resolution as @mxdvl mentioned.

I think, like @JamieB-gu mentioned, that we should investigate Option two, which is the only real reason you’d want to use the size attribute. This final output would be something like:

<picture>
    <source
        media="(min-width: 375px)">
        sizes="(-webkit-min-device-pixel-ratio: 1.25),(min-resolution: 120dpi)  640px, 320px"
        srcset="https://xxx.jpg?width=320px 320w, https://xxx.jpg?width=320px&dpr=2 640w"
    />
    <source
        media="(min-width: 740px)">
        sizes="(-webkit-min-device-pixel-ratio: 1.25),(min-resolution: 120dpi)  1200px, 600px"
        srcset="https://xxx.jpg?width=600px 600w, https://xxx.jpg?width=600px&dpr=2 1200w"
    />
    <source
        media="(min-width: 1200px)">
        sizes="(-webkit-min-device-pixel-ratio: 1.25),(min-resolution: 120dpi)  1800px, 900px"
        srcset="https://xxx.jpg?width=600px 900w, https://xxx.jpg?width=900px&dpr=2 1800w"
    />
</picture>
  • media relates to the size of the viewport and select the right source
  • sizes relates to the pixel density and picks the right srcset
  • srcset shows the possible versions of this image for this specific viewport/media attribute.

Comment on lines +33 to +35
The sizes html attribute acts as the translation layer between the size of the viewport and the size of the image source you'd like to pick. A good way to think about this is that, beyond a certain width, main media (inline) images never go beyond `620px` wide, and this html attribute gives us a way to communicate that to the browser.

For example `<source sizes="(min-width: 660px) 620px, 100vw">` tells the browser, "Hey, if your viewport is 660px or wider, always look for an image source which is 620px wide. If not, default to an image which is the same width as 100% of the viewport width". We can provide as many sizes & queries as we want, with the `(query) px, (query) px, ... , px)` syntax, where the last argument without a query is the default if none others match.
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth noting: it only makes sense to have multiple sizes if you have multiple srcset.

dotcom-rendering/docs/architecture/027-pictures.md Outdated Show resolved Hide resolved
dotcom-rendering/docs/architecture/027-pictures.md Outdated Show resolved Hide resolved
dotcom-rendering/docs/architecture/027-pictures.md Outdated Show resolved Hide resolved
Copy link
Contributor

@mxdvl mxdvl left a comment

Choose a reason for hiding this comment

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

Thinking about images again this morning, so wrote down a few ideas and comments.

I wanted to point out that the 85 and 45 quality values for images have not been tested extensively enough, and a associated piece of work would be to review these values and A/B test the performance with our Core Web Vitals metrics.

}
): Pixel => {
if (format.display === ArticleDisplay.Immersive && isMainMedia)
return breakpoint;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this right? Isn’t the desired size the whole width of the viewport?

Comment on lines +194 to +200
// Immersive MainMedia elements fill the height of the viewport, meaning
// on mobile devices even though the viewport width is small, we'll need
// a larger image to maintain quality. To solve this problem we're using
// the viewport height (vh) to calculate width. The value of 167vh
// relates to an assumed image ratio of 5:3 which is equal to
// 167 (viewport height) : 100 (viewport width).
sizes="167vh"
Copy link
Contributor

Choose a reason for hiding this comment

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

This is clever, but ideally we would have a portrait source. Otherwise the majority of the image downloaded isn’t even used.

To investigate in another PR, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed we could use resizer to cut off sides in this scenario. It could mean, browsers will have to download another image when orientation or window ratio changes, though. Worth investigating!

Also worth noting, the assumption of 5:3 crop, while practically often true at the Guardian, isn’t necessary as we carry real ratio in CAPI.

Copy link
Contributor

Choose a reason for hiding this comment

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

as we carry real ratio in CAPI

Oh, so we could replace 167 with a value calculated based on ratio? That seems like an easy win.

Copy link
Contributor

Choose a reason for hiding this comment

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

Waaaait… Why I can see aspectRatio only for the trail (always 5:3) image, but not for Main media which eg. here is a different ratio?! We could calculate from width and height, but I will ask CAPI why aspectRatio isn’t offered for all imagery.

Copy link
Contributor

Choose a reason for hiding this comment

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

Boo! It’s not in CAPI, coz it’s not in FlexibleAPI, coz it’s not in Media API (Grid). Quite a change, I suppose, but I will ask around. 🤦‍♂️

@paperboyo
Copy link
Contributor

I wanted to point out that the 85 and 45 quality values for images have not been tested extensively enough, and a associated piece of work would be to review these values and A/B test the performance with our Core Web Vitals metrics.

Thanks for having a look, Max! ❤️
I agree they haven’t been tested extensively enough. But they were tested enough to notice that even as-is some WebPs are already weightier than JPEGs, so we couldn’t increase their quality relative to the JPEGs much more if at all. Also, both formats, although WebP more severely, are definitely on the performance side, to put it mildly. Most WebPs are smeary, while some JPEGs exhibit posterisation. Hence, I don’t think measuring against metrics would gain us much. We need to measure against discerning humans caring about image quality more like.

Now, doing it before we will use AVIF makes little sense in my mind. Changing those values would decache all our imagery (even if we would earlier devise a way to test them less globally). I would argue, let’s have a look at quality values, dpr steps and the rest when we will be working on AVIF. Sadly, our image resizer is the slowest with offering support compared to (all?) competitors…

@OllysCoding OllysCoding merged commit d7921a4 into main Nov 29, 2021
@OllysCoding OllysCoding deleted the olly/picture-optimisations branch November 29, 2021 09:57
@paperboyo
Copy link
Contributor

@OllysCoding wins Jimmy’s Award for Best Described and Most Informative PRs 2021!

Congratulations on behalf of The Committee 🥇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants