ChannelThumbnail improvements

- [x] (6332) The IntersectionObserver method of lazy-loading loads cached images visibly late on slower devices. Previously, it was also showing the "broken image" icon briefly, which we mended by placing a dummy transparent image as the initial src.
  - Reverted that ugly transparent image fix.
  - Use the browser's built-in `loading="lazy"` instead. Sorry, Safari.

- [x] Size-optimization did not take "device pixel ratio" into account.
  - When resizing an image through the CDN, we can't just take the dimensions of the tag in pixels directly -- we need to take zooming into account, otherwise the image ends up blurry.
  - Previously, we quickly disabled optimization for the channel avatar in the Channel Page because of this. Now that we know the root-cause, the change was reverted and we now go through the CDN with appropriate sizes. This also improves our Web Vital scores.

- [x] Size-optimization wasn't really implemented for all ChannelThumbnail instances.
  - The CDN-optimized size was hardcoded to the largest instance, so small images like sidebar thumbnails are still loading images that are unnecessarily larger.
  - There's a little-bit of hardcoding of values from CSS here, but I think it's a ok compromise (not something we change often). It also doesn't need to be exact -- the "device pixel ratio" calculate will ensure it's slightly larger than what we need.

- [x] Set `width` and `height` of `<img>` to improve CLS.
  - Addresses Ligthhouse complaints, although technically the shifting was addressed at the `ClaimPreviewTile` level (sub-container dimensions are well defined).
  - Notes: the values don't need to be the final CSS-adjusted sizes. It just needs to be in the right aspect ratio to help the browser pre-allocate space to avoid shifts.

- [x] Add option to disable lazy-load Channel Thumbnails
  - The guidelines mentioned that items that are already in the viewport should not enable `loading="lazy"`.
  - We have a few areas where it doesn't make sense to lazy-load (e.g. thumbnail in Header, channel selector dropdown, publish preview, etc.).
This commit is contained in:
infinite-persistence 2021-07-05 13:20:40 +08:00
parent 6615f28ae1
commit fc7edc875b
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
13 changed files with 49 additions and 32 deletions

View file

@ -30,7 +30,7 @@ function ChannelListItem(props: ListItemProps) {
return ( return (
<div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}> <div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}>
<ChannelThumbnail uri={uri} hideStakedIndicator /> <ChannelThumbnail uri={uri} hideStakedIndicator xsmall noLazyLoad />
<ChannelTitle uri={uri} /> <ChannelTitle uri={uri} />
{isSelected && <Icon icon={ICONS.DOWN} />} {isSelected && <Icon icon={ICONS.DOWN} />}
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 B

View file

@ -3,11 +3,14 @@ import React from 'react';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import Gerbil from './gerbil.png'; import Gerbil from './gerbil.png';
import Transparent from './transparent_1x1.png';
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper'; import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
import ChannelStakedIndicator from 'component/channelStakedIndicator'; import ChannelStakedIndicator from 'component/channelStakedIndicator';
import { getThumbnailCdnUrl } from 'util/thumbnail'; import { getThumbnailCdnUrl } from 'util/thumbnail';
import useLazyLoading from 'effects/use-lazy-loading';
const FONT_PX = 16.0;
const IMG_XSMALL_REM = 2.1;
const IMG_SMALL_REM = 3.0;
const IMG_NORMAL_REM = 10.0;
type Props = { type Props = {
thumbnail: ?string, thumbnail: ?string,
@ -16,14 +19,14 @@ type Props = {
thumbnailPreview: ?string, thumbnailPreview: ?string,
obscure?: boolean, obscure?: boolean,
small?: boolean, small?: boolean,
xsmall?: boolean,
allowGifs?: boolean, allowGifs?: boolean,
claim: ?ChannelClaim, claim: ?ChannelClaim,
doResolveUri: (string) => void, doResolveUri: (string) => void,
isResolving: boolean, isResolving: boolean,
showDelayedMessage?: boolean, showDelayedMessage?: boolean,
noLazyLoad?: boolean,
hideStakedIndicator?: boolean, hideStakedIndicator?: boolean,
xsmall?: boolean,
noOptimization?: boolean,
}; };
function ChannelThumbnail(props: Props) { function ChannelThumbnail(props: Props) {
@ -40,8 +43,8 @@ function ChannelThumbnail(props: Props) {
doResolveUri, doResolveUri,
isResolving, isResolving,
showDelayedMessage = false, showDelayedMessage = false,
noLazyLoad,
hideStakedIndicator = false, hideStakedIndicator = false,
noOptimization,
} = props; } = props;
const [thumbError, setThumbError] = React.useState(false); const [thumbError, setThumbError] = React.useState(false);
const shouldResolve = claim === undefined; const shouldResolve = claim === undefined;
@ -51,6 +54,8 @@ function ChannelThumbnail(props: Props) {
const isGif = channelThumbnail && channelThumbnail.endsWith('gif'); const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview; const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
const thumbnailRef = React.useRef(null); const thumbnailRef = React.useRef(null);
const thumbnailSize = calcRenderedImgWidth(); // currently always 1:1
// Generate a random color class based on the first letter of the channel name // Generate a random color class based on the first letter of the channel name
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
let initializer; let initializer;
@ -62,14 +67,26 @@ function ChannelThumbnail(props: Props) {
colorClassName = `channel-thumbnail__default--4`; colorClassName = `channel-thumbnail__default--4`;
} }
function calcRenderedImgWidth() {
let rem;
if (xsmall) {
rem = IMG_XSMALL_REM;
} else if (small) {
rem = IMG_SMALL_REM;
} else {
rem = IMG_NORMAL_REM;
}
const devicePixelRatio = window.devicePixelRatio || 1.0;
return Math.ceil(rem * devicePixelRatio * FONT_PX);
}
React.useEffect(() => { React.useEffect(() => {
if (shouldResolve && uri) { if (shouldResolve && uri) {
doResolveUri(uri); doResolveUri(uri);
} }
}, [doResolveUri, shouldResolve, uri]); }, [doResolveUri, shouldResolve, uri]);
useLazyLoading(thumbnailRef, 0.25, [showThumb, thumbError, channelThumbnail]);
if (isGif && !allowGifs) { if (isGif && !allowGifs) {
return ( return (
<FreezeframeWrapper src={channelThumbnail} className={classnames('channel-thumbnail', className)}> <FreezeframeWrapper src={channelThumbnail} className={classnames('channel-thumbnail', className)}>
@ -81,8 +98,8 @@ function ChannelThumbnail(props: Props) {
let url = channelThumbnail; let url = channelThumbnail;
// @if TARGET='web' // @if TARGET='web'
// Pass image urls through a compression proxy, except for GIFs. // Pass image urls through a compression proxy, except for GIFs.
if (thumbnail && !noOptimization && !(isGif && allowGifs)) { if (thumbnail && !(isGif && allowGifs)) {
url = getThumbnailCdnUrl({ thumbnail }); url = getThumbnailCdnUrl({ thumbnail, width: thumbnailSize, height: thumbnailSize, quality: 85 });
} }
// @endif // @endif
@ -100,8 +117,10 @@ function ChannelThumbnail(props: Props) {
ref={thumbnailRef} ref={thumbnailRef}
alt={__('Channel profile picture')} alt={__('Channel profile picture')}
className="channel-thumbnail__default" className="channel-thumbnail__default"
data-src={!thumbError && url ? url : Gerbil} src={!thumbError && url ? url : Gerbil}
src={Transparent} width={thumbnailSize}
height={thumbnailSize}
loading={noLazyLoad ? undefined : 'lazy'}
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil. onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
/> />
)} )}
@ -114,8 +133,10 @@ function ChannelThumbnail(props: Props) {
ref={thumbnailRef} ref={thumbnailRef}
alt={__('Channel profile picture')} alt={__('Channel profile picture')}
className="channel-thumbnail__custom" className="channel-thumbnail__custom"
data-src={!thumbError && url ? url : Gerbil} src={!thumbError && url ? url : Gerbil}
src={Transparent} width={thumbnailSize}
height={thumbnailSize}
loading={noLazyLoad ? undefined : 'lazy'}
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil. onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
/> />
)} )}

View file

@ -300,7 +300,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
> >
{isChannelUri && claim ? ( {isChannelUri && claim ? (
<UriIndicator uri={uri} link> <UriIndicator uri={uri} link>
<ChannelThumbnail uri={uri} /> <ChannelThumbnail uri={uri} small={type === 'inline'} />
</UriIndicator> </UriIndicator>
) : ( ) : (
<> <>
@ -404,7 +404,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<div className="claim-preview__primary-actions"> <div className="claim-preview__primary-actions">
{!isChannelUri && signingChannel && ( {!isChannelUri && signingChannel && (
<div className="claim-preview__channel-staked"> <div className="claim-preview__channel-staked">
<ChannelThumbnail uri={signingChannel.permanent_url} /> <ChannelThumbnail uri={signingChannel.permanent_url} xsmall />
</div> </div>
)} )}

View file

@ -226,7 +226,7 @@ function ClaimPreviewTile(props: Props) {
) : ( ) : (
<React.Fragment> <React.Fragment>
<UriIndicator uri={uri} link hideAnonymous> <UriIndicator uri={uri} link hideAnonymous>
<ChannelThumbnail uri={channelUri} /> <ChannelThumbnail uri={channelUri} xsmall />
</UriIndicator> </UriIndicator>
<div className="claim-tile__about"> <div className="claim-tile__about">

View file

@ -182,9 +182,9 @@ function Comment(props: Props) {
> >
<div className="comment__thumbnail-wrapper"> <div className="comment__thumbnail-wrapper">
{authorUri ? ( {authorUri ? (
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} small className="comment__author-thumbnail" /> <ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="comment__author-thumbnail" />
) : ( ) : (
<ChannelThumbnail small className="comment__author-thumbnail" /> <ChannelThumbnail xsmall className="comment__author-thumbnail" />
)} )}
</div> </div>

View file

@ -218,7 +218,7 @@ function CommentMenuList(props: Props) {
{activeChannelClaim && ( {activeChannelClaim && (
<div className="comment__menu-active"> <div className="comment__menu-active">
<ChannelThumbnail uri={activeChannelClaim.permanent_url} /> <ChannelThumbnail xsmall noLazyLoad uri={activeChannelClaim.permanent_url} />
<div className="comment__menu-channel"> <div className="comment__menu-channel">
{__('Interacting as %channelName%', { channelName: activeChannelClaim.name })} {__('Interacting as %channelName%', { channelName: activeChannelClaim.name })}
</div> </div>

View file

@ -105,7 +105,9 @@ export default function CommentReactions(props: Props) {
className={classnames('comment__action comment__action--creator-like')} className={classnames('comment__action comment__action--creator-like')}
onClick={() => react(commentId, REACTION_TYPES.CREATOR_LIKE)} onClick={() => react(commentId, REACTION_TYPES.CREATOR_LIKE)}
> >
{creatorLiked && <ChannelThumbnail uri={authorUri} hideStakedIndicator className="comment__creator-like" />} {creatorLiked && (
<ChannelThumbnail xsmall uri={authorUri} hideStakedIndicator className="comment__creator-like" />
)}
</Button> </Button>
)} )}
</> </>

View file

@ -319,7 +319,7 @@ const Header = (props: Props) => {
// @endif // @endif
> >
{activeChannelUrl ? ( {activeChannelUrl ? (
<ChannelThumbnail uri={activeChannelUrl} /> <ChannelThumbnail uri={activeChannelUrl} small noLazyLoad />
) : ( ) : (
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden /> <Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
)} )}

View file

@ -485,7 +485,7 @@ function SubscriptionListItem({ subscription }: { subscription: Subscription })
className="navigation-link navigation-link--with-thumbnail" className="navigation-link navigation-link--with-thumbnail"
activeClass="navigation-link--active" activeClass="navigation-link--active"
> >
<ChannelThumbnail uri={uri} hideStakedIndicator /> <ChannelThumbnail xsmall uri={uri} hideStakedIndicator />
<span dir="auto" className="button__label"> <span dir="auto" className="button__label">
{channelName} {channelName}
</span> </span>

View file

@ -40,7 +40,7 @@ export default function WunderbarSuggestion(props: Props) {
'wunderbar__suggestion--channel': isChannel, 'wunderbar__suggestion--channel': isChannel,
})} })}
> >
{isChannel && <ChannelThumbnail uri={uri} />} {isChannel && <ChannelThumbnail small uri={uri} />}
{!isChannel && ( {!isChannel && (
<FileThumbnail uri={uri}> <FileThumbnail uri={uri}>
{/* @if TARGET='app' */} {/* @if TARGET='app' */}

View file

@ -199,7 +199,7 @@ const ModalPublishPreview = (props: Props) => {
const channelClaim = myChannels && myChannels.find((x) => x.name === channel); const channelClaim = myChannels && myChannels.find((x) => x.name === channel);
return channel ? ( return channel ? (
<div className="channel-value"> <div className="channel-value">
{channelClaim && <ChannelThumbnail uri={channelClaim.permanent_url} />} {channelClaim && <ChannelThumbnail xsmall noLazyLoad uri={channelClaim.permanent_url} />}
{channel} {channel}
</div> </div>
) : ( ) : (

View file

@ -185,13 +185,7 @@ function ChannelPage(props: Props) {
</div> </div>
{cover && <img className={classnames('channel-cover__custom')} src={cover} />} {cover && <img className={classnames('channel-cover__custom')} src={cover} />}
<div className="channel__primary-info"> <div className="channel__primary-info">
<ChannelThumbnail <ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs hideStakedIndicator />
className="channel__thumbnail--channel-page"
uri={uri}
allowGifs
hideStakedIndicator
noOptimization
/>
<h1 className="channel__title"> <h1 className="channel__title">
{title || '@' + channelName} {title || '@' + channelName}
<ChannelStakedIndicator uri={uri} large /> <ChannelStakedIndicator uri={uri} large />