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:
parent
6615f28ae1
commit
fc7edc875b
13 changed files with 49 additions and 32 deletions
|
@ -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 |
|
@ -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.
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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 />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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' */}
|
||||||
|
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
Loading…
Reference in a new issue