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

Use picture tag instead of img #2214

Merged
merged 6 commits into from
Dec 14, 2020
Merged

Conversation

oliverlloyd
Copy link
Contributor

@oliverlloyd oliverlloyd commented Dec 9, 2020

What?

This PR is part of the ongoing work to improve how images are rendered in DCR. Here we replace the img tag with a picture tag using multiple sources.

Why?

In the previous PR in this series we added a function to define a sizes array specific to DCR and applied that to an img tag. This was successful in improving the image chosen in many cases but we were still left with 2 issues:

  1. The Wealthy Guy problem. Mobile devices with small screens but high dpr values can end up being served excessively large files, wasting bandwidth.

  2. The DPR1/2 quality 45/85 thing. We want to be able to specify a different set of images to use above a certain resolution. This is because we've found that for high resolution screens, by using dpr=2&quality=45 together we can reduce the file size without noticeably impacting quality.

By reintroducing a picture tag we can now address point 2. We do this by having two sources under the picture tag, one for high dpi devices which uses the source sets that have the dpr=2&quality=45 structure, and the other for the rest, using quality=80.

What is different to Frontend?

One thing where this approach diverges from that used in Frontend is here we only set two source tags, one for hidpi images and one for the rest. But in Frontend two source tags are set for each breakpoint, like this:

    @widths.breakpoints.map { breakpointWidth =>
        <source media="(min-width: @breakpointWidth.breakpoint.minWidth.getOrElse("0")px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: @breakpointWidth.breakpoint.minWidth.getOrElse("0")px) and (min-resolution: 120dpi)"
        sizes="@breakpointWidth.width"
        srcset="@SrcSet.asSrcSetString(ImgSrc.srcsetForBreakpoint(breakpointWidth, widths.breakpoints, maybePath = None, maybeImageMedia = Some(picture), hidpi = true))" />
        <source media="(min-width: @breakpointWidth.breakpoint.minWidth.getOrElse("0")px)"
        sizes="@breakpointWidth.width"
        srcset="@SrcSet.asSrcSetString(ImgSrc.srcsetForBreakpoint(breakpointWidth, widths.breakpoints, maybePath = None, maybeImageMedia = Some(picture)))" />
    }

I'm curious what others think here (@JamieB-gu @gtrufitt ?) but my feeling is that we don't want to be so explicit in directing the browser. Frontend is dealing with absolutes, each sizes and srcset attribute on each source tag only ever contains one value, so the browser never has a choice. In this PR we are going from one set of sources (used by the img tag) to two, split by resolution. But for the rest we are still letting the browser make it's own choice. We give it hints in the form of an array of sizes but it is still free to make a different choice if it thinks it should. Reasons why browsers might decide to pick a different image than what might be expected include:

  • It already has a larger version in its cache
  • it thinks the network conditions are too poor to justify downloading a larger image so it degrades the image in favour of speed

These choices are not made by all browsers (perhaps even none of them) but they are part of the spec and we should offer browsers the chance to make them or others to improve the experience for the reader

Does it work?

This code was manually tested for all image roles, at all breakpoints and on both DPR2 and DPR1 devices 👍

What next?

The end goal for images in DCR is to use Image Rendering . Once we have a stable approach working locally for DCR we can decide how best to hoist this code up and what contract we want to use.

More investigation and testing is needed to quantify and decide an approach for the Wealthy Guy problem. I'm keen to make sure we learn from and respect the previous work in this area

@github-actions
Copy link

github-actions bot commented Dec 9, 2020

Size Change: +273 B (0%)

Total Size: 719 kB

Filename Size Change
dist/frontend.server.js 224 kB +269 B (0%)
dist/MostViewedRightWrapper.js 4.08 kB +2 B (0%)
dist/MostViewedRightWrapper.legacy.js 4.3 kB +2 B (0%)
ℹ️ View Unchanged
Filename Size Change
dist/atomIframe.js 1.21 kB 0 B
dist/atomIframe.legacy.js 1.21 kB 0 B
dist/dynamicImport.js 1.96 kB 0 B
dist/dynamicImport.legacy.js 2.01 kB 0 B
dist/embedIframe.js 1.21 kB 0 B
dist/embedIframe.legacy.js 1.22 kB 0 B
dist/ga.js 3.01 kB 0 B
dist/ga.legacy.js 3.47 kB 0 B
dist/GetMatchStats.js 3.23 kB 0 B
dist/GetMatchStats.legacy.js 3.32 kB 0 B
dist/lotame.js 1.14 kB 0 B
dist/lotame.legacy.js 1.15 kB 0 B
dist/MostViewedFooterData.js 5.63 kB 0 B
dist/MostViewedFooterData.legacy.js 5.84 kB 0 B
dist/newsletterEmbedIframe.js 1.14 kB 0 B
dist/newsletterEmbedIframe.legacy.js 1.15 kB 0 B
dist/OnwardsLower.js 8.8 kB 0 B
dist/OnwardsLower.legacy.js 9.06 kB 0 B
dist/OnwardsUpper.js 12.8 kB 0 B
dist/OnwardsUpper.legacy.js 13.2 kB 0 B
dist/ophan.js 6.94 kB 0 B
dist/ophan.legacy.js 6.93 kB 0 B
dist/react.js 114 kB 0 B
dist/react.legacy.js 117 kB 0 B
dist/sentry.js 22.8 kB 0 B
dist/sentry.legacy.js 22.7 kB 0 B
dist/shimport.js 3.21 kB 0 B
dist/shimport.legacy.js 3.21 kB 0 B
dist/SignInGateMain.js 1.87 kB 0 B
dist/SignInGateMain.legacy.js 1.89 kB 0 B
dist/vendors~braze-web-sdk-core.js 34.9 kB 0 B
dist/vendors~braze-web-sdk-core.legacy.js 34.9 kB 0 B
dist/vendors~guardian-braze-components.js 17.3 kB 0 B
dist/vendors~guardian-braze-components.legacy.js 17.4 kB 0 B

compressed-size-action

@oliverlloyd
Copy link
Contributor Author

Copy link
Contributor

@JamieB-gu JamieB-gu left a comment

Choose a reason for hiding this comment

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

Nice 🚀! This code is starting to converge with that in image-rendering, although there are a couple extra things that you do here that image-rendering could benefit from doing too 👍

I'm curious what others think here (@JamieB-gu @gtrufitt ?)

This looks sensible to me. It's the approach used in image-rendering for the reasons you described.

This code is starting to look a lot like what's happening in Apps-Rendering too! I've dropped a couple of comments to point to the equivalent code there. Convergent evolution gives me some confidence that we're all doing the right thing 🙂

@@ -13,6 +13,8 @@ type Props = {
isLazy?: boolean;
};

type ResolutionType = 'hdpi' | 'mdpi';
Copy link
Contributor

Choose a reason for hiding this comment

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

height={height}
width={width}
loading={
isLazy && !Picture.disableLazyLoading ? 'lazy' : undefined
Copy link
Contributor

@JamieB-gu JamieB-gu Dec 10, 2020

Choose a reason for hiding this comment

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

Ah! We should start using the loading attribute in image-rendering too!

guardian/image-rendering#151

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 This was one of my 'what do the open source image components do' points! It's a big perf win I think

Comment on lines +131 to +132
const hdpiSources = getSources(role, 'hdpi', imageSources);
const mdpiSources = getSources(role, 'mdpi', imageSources);
Copy link
Contributor

Choose a reason for hiding this comment

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

@oliverlloyd
Copy link
Contributor Author

This code is starting to look a lot like what's happening in Apps-Rendering too

This is great! At some point we should mob on working out a plan for how to start migrating

Copy link
Contributor

@liywjl liywjl left a comment

Choose a reason for hiding this comment

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

LGTM 👍 really great work as always!!

@paperboyo
Copy link
Contributor

paperboyo commented Dec 11, 2020

here we only set two source tags, one for hidpi images and one for the rest. But in Frontend two source tags are set for each breakpoint

So this will prevent us from ever showing a HiDPI asset on lower density screens (visual disaster) and ever showing LoDPI asset on higher density ones (wasted bandwidth)? Great!

(Our HiDPI and LoDPI varying quality assets were constructed such that they were intended to be displayed at the same physical dimensions and requested using the same width which is equal (or very close) to the CSS pixels width. If we stray from those constraints we may possibly find ourselves in a territory of either revealing compression artifacts (if the request’s width is lower than CSS width) or serving something too weighty (if the request’s width is higher than CSS width). We do not have qualitys suitable for steps in between. It was done mostly out of the hat, with just couple of images (one of Bernie Sanders!), but these were the intentions. It will need to be done again for AVIF.)

Giving browsers more choice sounds great, but my understanding of how this PR will differ practically from current frontend is too shallow to clearly imagine if this will prevent us also from:

  • serving anything that satisfies dpr>2 which seems a waste unless we care about desktop browsers zoomed in beyond 200% which we shouldn’t I don’t think
  • serving eg. a HiDPI source with width 620 for an image o 1020 CSS px, because it will be described as 1240w (a visual degradation compared to current frontend which would serve a 2040w asset there)

Isn’t that true that cache mostly matters if the exact crop used on a front is also later used within an article, but huge majority of front trail pictures are too small to be reused in articles and on your way back, it will most likely only be one main media (if they are exact same crop) that you would be able to reuse?

As to the network conditions: yeah, if that’s true, then more than just a choice of two pictures would indeed give as a faster site with lower quality imagery. But isn’t this approach of mixing sizes between breakpoints give us less control over just how worse? If network sniffing is a thing, maybe we could introduce a specifically designed third source, in addition to frontend’s two, that would provide consistently worse visual experience (I would argue that we are set on the performance side already, as some sunsets with subtle gradients well illustrate, though).

All that may not apply to “fluid” images whose dimensions depend on viewport size. Here, while still not mixing HiDPI and LoDPI, we definitely need more steps. But here also, I would argue it’s better to design the number of them and their distribution explicitly and consistently, not by unpredictably mixing sizes from different breakpoints (of CSS widths varying how designers seen fit).

I know this may all sound idiotic for at least two reasons: it seems to treat what we do in current frontend as gospel and also it seems to go against most of the guides about using responsive images on the interwebz. But I fail to really see a hole in the above logic. Apart from me not being to lay it out neither clearly nor succinctly. I would be extremely grateful and excited to hear counter arguments. Even better: a total takedown! I hate gospel. Gospel is for the weak.

EDIT: shit, ’ts late! Cyberpunk’s fault! It’s not as bad as I feared.

@oliverlloyd
Copy link
Contributor Author

oliverlloyd commented Dec 11, 2020

Point by point

If we stray from those constraints we may possibly find ourselves in a territory of either revealing compression artifacts (if the request’s width is lower than CSS width) or serving something too weighty (if the request’s width is higher than CSS width)

On the first point, serving images that are too small. That shouldn't happen. The spec for srcset makes it so that browsers will read the sizes hints, selecting the first media query that matches (eg: (min-width: 620px) 620px and then it will look in the array of images it has to choose from (which it gets from srcset + whatever is in the cache) and either picks the one with a width of 620 or the next largest one, not the closest.

To the second point, serving something too weighty if request width > css width. Is this the DPR4 problem? As things stand, this implementation does not offer a solution to this problem (point 1 in the description). If not, then I'm misunderstanding the problem and need more help!

serving anything that satisfies dpr>2

So yeah, this is point 1 ^^. I suspect you may well be right in thinking we need to be more explicit if we want to solve this problem. I deliberately left it off from this PR though because a) we want to provide immediate benefit by solving the hdpi/mdpi problem but also b) because I think it would be good to tackle that one in it's own PR. Perhaps we end up right back at what Frontend is doing but it'd be good to step through that journey

serving eg. a HiDPI source with width 620 for an image o 1020 CSS px, because it will be described as 1240w

Based on my testing, and my understanding (more the former if I'm honest), this won't happen. In most cases the code in this PR results in the same image being rendered as in frontend, both for dpr1 and dpr2. Where they differ its because the array of sources given to DCR is different and the choice is logical given DCR's constraints (next largest).

majority of front trail pictures are too small to be reused

This is a good point. You're right, in most real life situations this pick from cache feature won't help much. But it is a performance gain, even if just a small one.

network conditions

I don't believe any browser is currently implementing this. But if they did, I'd like to think one of the ways they could do this would be to solve the DPR4 problem.

In general

Putting aside the DPR4 problem, my feeling is we want to stay as close to the intended use for things like sizes and srcset. The whatwg org has open issues around how to make image loading better that deal with the DPR4 problem and other areas. Both of these propose addressing things by leaning into letting the browser choose.

In Frontend we go the other direction by only ever setting one option in the sizes array as a way to essentially turn off that feature. The benefit here is we have absolute control but this has a real cost. We need to maintain this code for every page layout changeand it's hard to automate testing in this area so our exposure to bugs is high.

All that said, I'm not currently aware of a solution to the DPR4 problem that doesn't invovles fewer sizes and more sources so this could all be moot!

@paperboyo
Copy link
Contributor

serving eg. a HiDPI source with width 620 for an image o 1020 CSS px, because it will be described as 1240w (a visual degradation compared to current frontend which would serve a 2040w asset there)

serving images that are too small. That shouldn't happen.

Brilliant! This was my biggest worry. Because I’m on fiber and I’m selfish 😜. But also, more seriously, because some users noticed some degradation already.

Thanks so much for detailed response. I agree the intention of the spec is good. I guess my points can be summarised as: nothing against giving browsers more choice, but let’s be in control of that. One day we may find we need to add more dpr/quality ratios to our arsenal (which would mean extending and spending more time in @philmcmahon’s tool, which we’ll gonna have to do for AVIFs anyway).

@oliverlloyd
Copy link
Contributor Author

@gtrufitt @jfsoul happy to merge this one? This code is a clear step forward in terms of fixing the hdpi/mdpi problem so worth progressing with I think but there's still a strong case for having additional sources that this work should not be seem as discounting. I'd like to review the approach for this though and a new PR would be a nice way to do that.

Copy link
Contributor

@gtrufitt gtrufitt left a comment

Choose a reason for hiding this comment

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

👍 Aces

// Get the sources for this role
const sourcesForRole: SrcSetItem[] = getSourcesForRole(imageSources, role);
// Filter by resolution
const sourcesForResolution: SrcSetItem[] = getSourcesForResolution(
Copy link
Contributor

Choose a reason for hiding this comment

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

sourcesForRoleAndResolution maybe?

@oliverlloyd oliverlloyd merged commit 76c21d0 into main Dec 14, 2020
@oliverlloyd oliverlloyd deleted the oliver/replace-img-with-picture branch December 14, 2020 12:01
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.

6 participants