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 (
<div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}>
<ChannelThumbnail uri={uri} hideStakedIndicator />
<ChannelThumbnail uri={uri} hideStakedIndicator xsmall noLazyLoad />
<ChannelTitle uri={uri} />
{isSelected && <Icon icon={ICONS.DOWN} />}
</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 classnames from 'classnames';
import Gerbil from './gerbil.png';
import Transparent from './transparent_1x1.png';
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
import ChannelStakedIndicator from 'component/channelStakedIndicator';
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 = {
thumbnail: ?string,
@ -16,14 +19,14 @@ type Props = {
thumbnailPreview: ?string,
obscure?: boolean,
small?: boolean,
xsmall?: boolean,
allowGifs?: boolean,
claim: ?ChannelClaim,
doResolveUri: (string) => void,
isResolving: boolean,
showDelayedMessage?: boolean,
noLazyLoad?: boolean,
hideStakedIndicator?: boolean,
xsmall?: boolean,
noOptimization?: boolean,
};
function ChannelThumbnail(props: Props) {
@ -40,8 +43,8 @@ function ChannelThumbnail(props: Props) {
doResolveUri,
isResolving,
showDelayedMessage = false,
noLazyLoad,
hideStakedIndicator = false,
noOptimization,
} = props;
const [thumbError, setThumbError] = React.useState(false);
const shouldResolve = claim === undefined;
@ -51,6 +54,8 @@ function ChannelThumbnail(props: Props) {
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
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
const { channelName } = parseURI(uri);
let initializer;
@ -62,14 +67,26 @@ function ChannelThumbnail(props: Props) {
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(() => {
if (shouldResolve && uri) {
doResolveUri(uri);
}
}, [doResolveUri, shouldResolve, uri]);
useLazyLoading(thumbnailRef, 0.25, [showThumb, thumbError, channelThumbnail]);
if (isGif && !allowGifs) {
return (
<FreezeframeWrapper src={channelThumbnail} className={classnames('channel-thumbnail', className)}>
@ -81,8 +98,8 @@ function ChannelThumbnail(props: Props) {
let url = channelThumbnail;
// @if TARGET='web'
// Pass image urls through a compression proxy, except for GIFs.
if (thumbnail && !noOptimization && !(isGif && allowGifs)) {
url = getThumbnailCdnUrl({ thumbnail });
if (thumbnail && !(isGif && allowGifs)) {
url = getThumbnailCdnUrl({ thumbnail, width: thumbnailSize, height: thumbnailSize, quality: 85 });
}
// @endif
@ -100,8 +117,10 @@ function ChannelThumbnail(props: Props) {
ref={thumbnailRef}
alt={__('Channel profile picture')}
className="channel-thumbnail__default"
data-src={!thumbError && url ? url : Gerbil}
src={Transparent}
src={!thumbError && url ? url : Gerbil}
width={thumbnailSize}
height={thumbnailSize}
loading={noLazyLoad ? undefined : 'lazy'}
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
/>
)}
@ -114,8 +133,10 @@ function ChannelThumbnail(props: Props) {
ref={thumbnailRef}
alt={__('Channel profile picture')}
className="channel-thumbnail__custom"
data-src={!thumbError && url ? url : Gerbil}
src={Transparent}
src={!thumbError && url ? url : Gerbil}
width={thumbnailSize}
height={thumbnailSize}
loading={noLazyLoad ? undefined : 'lazy'}
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 ? (
<UriIndicator uri={uri} link>
<ChannelThumbnail uri={uri} />
<ChannelThumbnail uri={uri} small={type === 'inline'} />
</UriIndicator>
) : (
<>
@ -404,7 +404,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<div className="claim-preview__primary-actions">
{!isChannelUri && signingChannel && (
<div className="claim-preview__channel-staked">
<ChannelThumbnail uri={signingChannel.permanent_url} />
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall />
</div>
)}

View file

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

View file

@ -182,9 +182,9 @@ function Comment(props: Props) {
>
<div className="comment__thumbnail-wrapper">
{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>

View file

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

View file

@ -105,7 +105,9 @@ export default function CommentReactions(props: Props) {
className={classnames('comment__action comment__action--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>
)}
</>

View file

@ -319,7 +319,7 @@ const Header = (props: Props) => {
// @endif
>
{activeChannelUrl ? (
<ChannelThumbnail uri={activeChannelUrl} />
<ChannelThumbnail uri={activeChannelUrl} small noLazyLoad />
) : (
<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"
activeClass="navigation-link--active"
>
<ChannelThumbnail uri={uri} hideStakedIndicator />
<ChannelThumbnail xsmall uri={uri} hideStakedIndicator />
<span dir="auto" className="button__label">
{channelName}
</span>

View file

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

View file

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

View file

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