Lazy load images (#4228)

* Add lazy loading for many images
* Load sprites on first hover of scrubber
This commit is contained in:
WithoutPants
2023-10-23 16:52:56 +11:00
committed by GitHub
parent 87bdbb2058
commit 298f3d4e19
18 changed files with 72 additions and 11 deletions

View File

@@ -99,6 +99,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
) : ( ) : (
<img <img
className="image-thumbnail" className="image-thumbnail"
loading="lazy"
alt={props.gallery.studio.name} alt={props.gallery.studio.name}
src={props.gallery.studio.image_path ?? ""} src={props.gallery.studio.image_path ?? ""}
/> />
@@ -153,6 +154,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
<> <>
{props.gallery.cover ? ( {props.gallery.cover ? (
<img <img
loading="lazy"
className="gallery-card-image" className="gallery-card-image"
alt={props.gallery.title ?? ""} alt={props.gallery.title ?? ""}
src={`${props.gallery.cover.paths.thumbnail}`} src={`${props.gallery.cover.paths.thumbnail}`}

View File

@@ -168,6 +168,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
<Link to={`/galleries/${gallery.id}`}> <Link to={`/galleries/${gallery.id}`}>
{gallery.cover ? ( {gallery.cover ? (
<img <img
loading="lazy"
alt={gallery.title ?? ""} alt={gallery.title ?? ""}
className="w-100 w-sm-auto" className="w-100 w-sm-auto"
src={`${gallery.cover.paths.thumbnail}`} src={`${gallery.cover.paths.thumbnail}`}

View File

@@ -51,7 +51,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
tabIndex={0} tabIndex={0}
> >
<RatingSystem value={gallery.rating100 ?? undefined} disabled /> <RatingSystem value={gallery.rating100 ?? undefined} disabled />
<img src={cover} alt="" className={CLASSNAME_IMG} /> <img loading="lazy" src={cover} alt="" className={CLASSNAME_IMG} />
<footer className={CLASSNAME_FOOTER}> <footer className={CLASSNAME_FOOTER}>
<Link <Link
to={`/galleries/${gallery.id}`} to={`/galleries/${gallery.id}`}

View File

@@ -76,6 +76,7 @@ export const MovieCard: React.FC<IProps> = (props: IProps) => {
image={ image={
<> <>
<img <img
loading="lazy"
className="movie-card-image" className="movie-card-image"
alt={props.movie.name ?? ""} alt={props.movie.name ?? ""}
src={props.movie.front_image_path ?? ""} src={props.movie.front_image_path ?? ""}

View File

@@ -266,6 +266,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
image={ image={
<> <>
<img <img
loading="lazy"
className="performer-card-image" className="performer-card-image"
alt={performer.name ?? ""} alt={performer.name ?? ""}
src={performer.image_path ?? ""} src={performer.image_path ?? ""}

View File

@@ -56,6 +56,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
<td> <td>
<Link to={`/performers/${performer.id}`}> <Link to={`/performers/${performer.id}`}>
<img <img
loading="lazy"
className="image-thumbnail" className="image-thumbnail"
alt={performer.name ?? ""} alt={performer.name ?? ""}
src={performer.image_path ?? ""} src={performer.image_path ?? ""}

View File

@@ -1,7 +1,14 @@
import React, { useRef, useMemo, useState, useLayoutEffect } from "react"; import React, {
useRef,
useMemo,
useState,
useLayoutEffect,
useEffect,
} from "react";
import { useSpriteInfo } from "src/hooks/sprite"; import { useSpriteInfo } from "src/hooks/sprite";
import { useThrottle } from "src/hooks/throttle"; import { useThrottle } from "src/hooks/throttle";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import cx from "classnames";
interface IHoverScrubber { interface IHoverScrubber {
totalSprites: number; totalSprites: number;
@@ -62,7 +69,11 @@ const HoverScrubber: React.FC<IHoverScrubber> = ({
}, [activeIndex, totalSprites]); }, [activeIndex, totalSprites]);
return ( return (
<div className="hover-scrubber"> <div
className={cx("hover-scrubber", {
"hover-scrubber-inactive": !totalSprites,
})}
>
<div <div
className="hover-scrubber-area" className="hover-scrubber-area"
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
@@ -109,7 +120,9 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
const debounceSetActiveIndex = useThrottle(setActiveIndex, 50); const debounceSetActiveIndex = useThrottle(setActiveIndex, 50);
const spriteInfo = useSpriteInfo(vttPath); // hold off on loading vtt until first mouse over
const [hasLoaded, setHasLoaded] = useState(false);
const spriteInfo = useSpriteInfo(hasLoaded ? vttPath : undefined);
const sprite = useMemo(() => { const sprite = useMemo(() => {
if (!spriteInfo || activeIndex === undefined) { if (!spriteInfo || activeIndex === undefined) {
@@ -118,6 +131,13 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
return spriteInfo[activeIndex]; return spriteInfo[activeIndex];
}, [activeIndex, spriteInfo]); }, [activeIndex, spriteInfo]);
// mark as loaded on the first hover
useEffect(() => {
if (activeIndex !== undefined) {
setHasLoaded(true);
}
}, [activeIndex]);
useLayoutEffect(() => { useLayoutEffect(() => {
const imageParent = imageParentRef.current; const imageParent = imageParentRef.current;
@@ -153,7 +173,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
onClick(sprite.start); onClick(sprite.start);
} }
if (!spriteInfo) return null; if (!spriteInfo && hasLoaded) return null;
return ( return (
<div className="preview-scrubber"> <div className="preview-scrubber">
@@ -166,7 +186,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
</div> </div>
)} )}
<HoverScrubber <HoverScrubber
totalSprites={spriteInfo.length} totalSprites={spriteInfo?.length ?? 0}
activeIndex={activeIndex} activeIndex={activeIndex}
setActiveIndex={(i) => debounceSetActiveIndex(i)} setActiveIndex={(i) => debounceSetActiveIndex(i)}
onClick={onScrubberClick} onClick={onScrubberClick}

View File

@@ -71,7 +71,12 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
return ( return (
<div className={cx("scene-card-preview", { portrait: isPortrait })}> <div className={cx("scene-card-preview", { portrait: isPortrait })}>
<img className="scene-card-preview-image" src={image} alt="" /> <img
className="scene-card-preview-image"
loading="lazy"
src={image}
alt=""
/>
<video <video
disableRemotePlayback disableRemotePlayback
playsInline playsInline
@@ -166,7 +171,12 @@ export const SceneCard: React.FC<ISceneCardProps> = (
} }
return ( return (
<img className="image-thumbnail" alt={studioName} src={studioImage} /> <img
className="image-thumbnail"
loading="lazy"
alt={studioName}
src={studioImage}
/>
); );
} }

View File

@@ -88,7 +88,11 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
> >
<div className="ml-1 d-flex align-items-center"> <div className="ml-1 d-flex align-items-center">
<div className="thumbnail-container"> <div className="thumbnail-container">
<img alt={scene.title ?? ""} src={scene.paths.screenshot ?? ""} /> <img
loading="lazy"
alt={scene.title ?? ""}
src={scene.paths.screenshot ?? ""}
/>
</div> </div>
<div> <div>
<span className="align-middle text-break"> <span className="align-middle text-break">

View File

@@ -87,6 +87,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
<td> <td>
<Link to={sceneLink}> <Link to={sceneLink}>
<img <img
loading="lazy"
className="image-thumbnail" className="image-thumbnail"
alt={title} alt={title}
src={scene.paths.screenshot ?? ""} src={scene.paths.screenshot ?? ""}

View File

@@ -686,6 +686,16 @@ input[type="range"].blue-slider {
z-index: 1; z-index: 1;
} }
&.hover-scrubber-inactive {
.hover-scrubber-area {
cursor: inherit;
}
.hover-scrubber-indicator {
background-color: inherit;
}
}
.hover-scrubber-indicator { .hover-scrubber-indicator {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
bottom: -100%; bottom: -100%;

View File

@@ -85,6 +85,7 @@ export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
if (props.interactiveHeatmap) { if (props.interactiveHeatmap) {
return ( return (
<img <img
loading="lazy"
src={props.interactiveHeatmap} src={props.interactiveHeatmap}
alt="interactive heatmap" alt="interactive heatmap"
className="interactive-heatmap" className="interactive-heatmap"

View File

@@ -160,6 +160,7 @@ export const StudioCard: React.FC<IProps> = ({
linkClassName="studio-card-header" linkClassName="studio-card-header"
image={ image={
<img <img
loading="lazy"
className="studio-card-image" className="studio-card-image"
alt={studio.name} alt={studio.name}
src={studio.image_path ?? ""} src={studio.image_path ?? ""}

View File

@@ -49,6 +49,7 @@ const TaggerSceneDetails: React.FC<ITaggerSceneDetails> = ({ scene }) => {
className="performer-tag col m-auto zoom-2" className="performer-tag col m-auto zoom-2"
> >
<img <img
loading="lazy"
className="image-thumbnail" className="image-thumbnail"
alt={performer.name ?? ""} alt={performer.name ?? ""}
src={performer.image_path ?? ""} src={performer.image_path ?? ""}

View File

@@ -102,7 +102,12 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
key={p.remote_site_id} key={p.remote_site_id}
onClick={() => setModalStudio(p)} onClick={() => setModalStudio(p)}
> >
<img src={(p.image ?? [])[0]} alt="" className="StudioTagger-thumb" /> <img
loading="lazy"
src={(p.image ?? [])[0]}
alt=""
className="StudioTagger-thumb"
/>
<span>{p.name}</span> <span>{p.name}</span>
</Button> </Button>
)); ));

View File

@@ -611,7 +611,7 @@ const StudioTaggerList: React.FC<IStudioTaggerListProps> = ({
<div></div> <div></div>
<div> <div>
<Card className="studio-card"> <Card className="studio-card">
<img src={studio.image_path ?? ""} alt="" /> <img loading="lazy" src={studio.image_path ?? ""} alt="" />
</Card> </Card>
</div> </div>
<div className={`${CLASSNAME}-details-text`}> <div className={`${CLASSNAME}-details-text`}>

View File

@@ -187,6 +187,7 @@ export const TagCard: React.FC<IProps> = ({
linkClassName="tag-card-header" linkClassName="tag-card-header"
image={ image={
<img <img
loading="lazy"
className="tag-card-image" className="tag-card-image"
alt={tag.name} alt={tag.name}
src={tag.image_path ?? ""} src={tag.image_path ?? ""}

View File

@@ -66,6 +66,7 @@ const Preview: React.FC<{
const image = ( const image = (
<img <img
loading="lazy"
alt="" alt=""
className="wall-item-media" className="wall-item-media"
src={ src={