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

View File

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

View File

@@ -51,7 +51,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
tabIndex={0}
>
<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}>
<Link
to={`/galleries/${gallery.id}`}

View File

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

View File

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

View File

@@ -56,6 +56,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
<td>
<Link to={`/performers/${performer.id}`}>
<img
loading="lazy"
className="image-thumbnail"
alt={performer.name ?? ""}
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 { useThrottle } from "src/hooks/throttle";
import TextUtils from "src/utils/text";
import cx from "classnames";
interface IHoverScrubber {
totalSprites: number;
@@ -62,7 +69,11 @@ const HoverScrubber: React.FC<IHoverScrubber> = ({
}, [activeIndex, totalSprites]);
return (
<div className="hover-scrubber">
<div
className={cx("hover-scrubber", {
"hover-scrubber-inactive": !totalSprites,
})}
>
<div
className="hover-scrubber-area"
onMouseMove={onMouseMove}
@@ -109,7 +120,9 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
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(() => {
if (!spriteInfo || activeIndex === undefined) {
@@ -118,6 +131,13 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
return spriteInfo[activeIndex];
}, [activeIndex, spriteInfo]);
// mark as loaded on the first hover
useEffect(() => {
if (activeIndex !== undefined) {
setHasLoaded(true);
}
}, [activeIndex]);
useLayoutEffect(() => {
const imageParent = imageParentRef.current;
@@ -153,7 +173,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
onClick(sprite.start);
}
if (!spriteInfo) return null;
if (!spriteInfo && hasLoaded) return null;
return (
<div className="preview-scrubber">
@@ -166,7 +186,7 @@ export const PreviewScrubber: React.FC<IScenePreviewProps> = ({
</div>
)}
<HoverScrubber
totalSprites={spriteInfo.length}
totalSprites={spriteInfo?.length ?? 0}
activeIndex={activeIndex}
setActiveIndex={(i) => debounceSetActiveIndex(i)}
onClick={onScrubberClick}

View File

@@ -71,7 +71,12 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
return (
<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
disableRemotePlayback
playsInline
@@ -166,7 +171,12 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}
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="thumbnail-container">
<img alt={scene.title ?? ""} src={scene.paths.screenshot ?? ""} />
<img
loading="lazy"
alt={scene.title ?? ""}
src={scene.paths.screenshot ?? ""}
/>
</div>
<div>
<span className="align-middle text-break">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,7 +102,12 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
key={p.remote_site_id}
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>
</Button>
));

View File

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

View File

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

View File

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