From 547ea8e824e3549c1c4db0169716cf0acf94e2d5 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 30 Aug 2021 18:11:55 +0200 Subject: [PATCH] Refactor image lazy loading, this fixes #324 --- ui/package.json | 2 +- ui/src/Pages/AlbumPage/AlbumPage.tsx | 14 +-- ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx | 13 +-- .../photoGallery/MediaThumbnail.tsx | 14 +-- .../photoGallery/ProtectedMedia.tsx | 90 +++++++++++++------ .../timelineGallery/TimelineGallery.tsx | 9 -- ui/src/helpers/LazyLoad.ts | 57 ------------ 7 files changed, 72 insertions(+), 127 deletions(-) delete mode 100644 ui/src/helpers/LazyLoad.ts diff --git a/ui/package.json b/ui/package.json index e4ff9a9b..0d823f10 100644 --- a/ui/package.json +++ b/ui/package.json @@ -54,7 +54,7 @@ "url-join": "^4.0.1" }, "scripts": { - "start": "BROWSER=none craco start", + "start": "BROWSER=none PORT=1234 craco start", "build": "craco build", "test": "npm run lint && npm run jest -- --watchAll=false", "test:ci": "npm run lint && npm run jest:ci", diff --git a/ui/src/Pages/AlbumPage/AlbumPage.tsx b/ui/src/Pages/AlbumPage/AlbumPage.tsx index b7b141c5..5877e437 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.tsx +++ b/ui/src/Pages/AlbumPage/AlbumPage.tsx @@ -1,11 +1,10 @@ -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' import { useQuery, gql } from '@apollo/client' import AlbumGallery from '../../components/albumGallery/AlbumGallery' import Layout from '../../components/layout/Layout' import useURLParameters from '../../hooks/useURLParameters' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' -import LazyLoad from '../../helpers/LazyLoad' import { useTranslation } from 'react-i18next' import { albumQuery, albumQueryVariables } from './__generated__/albumQuery' import useOrderingParams from '../../hooks/useOrderingParams' @@ -125,17 +124,6 @@ function AlbumPage({ match }: AlbumPageProps) { [setOnlyFavorites, refetch] ) - useEffect(() => { - LazyLoad.loadImages(document.querySelectorAll('img[data-src]')) - return () => LazyLoad.disconnect() - }, []) - - useEffect(() => { - if (!loading) { - LazyLoad.loadImages(document.querySelectorAll('img[data-src]')) - } - }, [finishedLoadingMore, onlyFavorites, loading]) - if (error) return
Error
return ( diff --git a/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx b/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx index 814bbdcf..01978924 100644 --- a/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx +++ b/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx @@ -1,8 +1,7 @@ -import React, { useEffect } from 'react' +import React from 'react' import AlbumBoxes from '../../components/albumGallery/AlbumBoxes' import Layout from '../../components/layout/Layout' import { useQuery, gql } from '@apollo/client' -import LazyLoad from '../../helpers/LazyLoad' import { getMyAlbums } from './__generated__/getMyAlbums' const getAlbumsQuery = gql` @@ -20,15 +19,7 @@ const getAlbumsQuery = gql` ` const AlbumsPage = () => { - const { loading, error, data } = useQuery(getAlbumsQuery) - - useEffect(() => { - return () => LazyLoad.disconnect() - }, []) - - useEffect(() => { - !loading && LazyLoad.loadImages(document.querySelectorAll('img[data-src]')) - }, [loading]) + const { error, data } = useQuery(getAlbumsQuery) return ( diff --git a/ui/src/components/photoGallery/MediaThumbnail.tsx b/ui/src/components/photoGallery/MediaThumbnail.tsx index eb97d662..e9df158d 100644 --- a/ui/src/components/photoGallery/MediaThumbnail.tsx +++ b/ui/src/components/photoGallery/MediaThumbnail.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import React from 'react' import styled from 'styled-components' import { ProtectedImage } from './ProtectedMedia' import { MediaType } from '../../__generated__/globalTypes' @@ -14,12 +14,11 @@ const MediaContainer = styled.div` overflow: hidden; ` -const StyledPhoto = styled(ProtectedImage)<{ loaded: boolean }>` +const StyledPhoto = styled(ProtectedImage)` height: 200px; min-width: 100%; position: relative; object-fit: cover; - opacity: ${({ loaded }) => (loaded ? 1 : 0)}; transition: opacity 300ms; ` @@ -29,14 +28,7 @@ type LazyPhotoProps = { } const LazyPhoto = (photoProps: LazyPhotoProps) => { - const [loaded, setLoaded] = useState(false) - const onLoad = useCallback(e => { - !e.target.dataset.src && setLoaded(true) - }, []) - - return ( - - ) + return } const PhotoOverlay = styled.div<{ active: boolean }>` diff --git a/ui/src/components/photoGallery/ProtectedMedia.tsx b/ui/src/components/photoGallery/ProtectedMedia.tsx index bd95cfc9..06fc3ded 100644 --- a/ui/src/components/photoGallery/ProtectedMedia.tsx +++ b/ui/src/components/photoGallery/ProtectedMedia.tsx @@ -1,7 +1,11 @@ +import classNames from 'classnames' import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react' +import { useRef } from 'react' +import { useState } from 'react' +import { useEffect } from 'react' import { isNil } from '../../helpers/utils' -const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype +const isNativeLazyLoadSupported = 'loading' in document.createElement('img') const placeholder = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' @@ -27,7 +31,6 @@ export interface ProtectedImageProps src?: string key?: string lazyLoading?: boolean - loaded?: boolean } /** @@ -38,41 +41,78 @@ export interface ProtectedImageProps */ export const ProtectedImage = ({ src, - key, lazyLoading, - loaded, ...props }: ProtectedImageProps) => { - const lazyLoadProps: { 'data-src'?: string; loading?: 'lazy' | 'eager' } = {} + const url = getProtectedUrl(src) || placeholder - if (!isNativeLazyLoadSupported && lazyLoading) { - lazyLoadProps['data-src'] = getProtectedUrl(src) + if (!lazyLoading) { + return ( + + ) } - if (isNativeLazyLoadSupported && lazyLoading) { - lazyLoadProps.loading = 'lazy' + if (!isNativeLazyLoadSupported) { + return } - const imgSrc: string = - lazyLoading && !isNativeLazyLoadSupported - ? placeholder - : getProtectedUrl(src) || placeholder - - const loadedProp = - loaded !== undefined ? { loaded: loaded.toString() } : undefined - + // load with native lazy loading return ( - + ) } +interface FallbackLazyloadedImageProps + extends Omit< + DetailedHTMLProps, HTMLImageElement>, + 'src' + > { + src?: string +} + +const FallbackLazyloadedImage = ({ + src, + ...props +}: FallbackLazyloadedImageProps) => { + const [inView, setInView] = useState(false) + const imgRef = useRef(null) + + useEffect(() => { + const imgElm = imgRef.current + if (isNil(imgElm) || inView) return + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setInView(true) + observer.disconnect() + } + }, + { + root: null, + threshold: 0, + } + ) + + observer.observe(imgElm) + + return () => { + observer.disconnect() + } + }, [imgRef]) + + if (inView) { + return + } else { + return ( +
+ ) + } +} + export interface ProtectedVideoProps_Media { __typename: 'Media' id: string diff --git a/ui/src/components/timelineGallery/TimelineGallery.tsx b/ui/src/components/timelineGallery/TimelineGallery.tsx index eb7c5ffd..81e29125 100644 --- a/ui/src/components/timelineGallery/TimelineGallery.tsx +++ b/ui/src/components/timelineGallery/TimelineGallery.tsx @@ -7,7 +7,6 @@ import useURLParameters from '../../hooks/useURLParameters' import { FavoritesCheckbox } from '../album/AlbumFilter' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../PaginateLoader' -import LazyLoad from '../../helpers/LazyLoad' import { useTranslation } from 'react-i18next' import { myTimeline, @@ -141,14 +140,6 @@ const TimelineGallery = () => { }) }, [onlyFavorites]) - useEffect(() => { - !loading && LazyLoad.loadImages(document.querySelectorAll('img[data-src]')) - }, [finishedLoadingMore, onlyFavorites, loading]) - - useEffect(() => { - return () => LazyLoad.disconnect() - }, []) - if (error) { return
{error.message}
} diff --git a/ui/src/helpers/LazyLoad.ts b/ui/src/helpers/LazyLoad.ts deleted file mode 100644 index c071bd1c..00000000 --- a/ui/src/helpers/LazyLoad.ts +++ /dev/null @@ -1,57 +0,0 @@ -class LazyLoad { - observer: null | IntersectionObserver - - constructor() { - this.observe = this.observe.bind(this) - this.loadImages = this.loadImages.bind(this) - this.disconnect = this.disconnect.bind(this) - this.observer = null - } - - observe(images: Element[]) { - if (!this.observer) { - this.observer = new IntersectionObserver(entries => { - entries.forEach(entry => { - if (entry.isIntersecting || entry.intersectionRatio > 0) { - const element = entry.target - this.setSrcAttribute(element) - this.observer?.unobserve(element) - } - }) - }) - } - Array.from(images).forEach(image => this.observer?.observe(image)) - } - - loadImages(elements: NodeListOf) { - const images = Array.from(elements) - if (images.length) { - if ('IntersectionObserver' in window) { - this.observe(images) - } else { - images.forEach(image => this.setSrcAttribute(image)) - } - } - } - - disconnect() { - this.observer && this.observer.disconnect() - } - - setSrcAttribute(element: Element) { - if (element.hasAttribute('data-src')) { - const src = element.getAttribute('data-src') - if (src) { - element.removeAttribute('data-src') - element.setAttribute('src', src) - } else { - console.warn( - 'WARN: expected element to have `data-src` property', - element - ) - } - } - } -} - -export default new LazyLoad()