1
Fork 0

Merge pull request #484 from photoview/fix-324

Refactor image lazy loading
This commit is contained in:
Viktor Strate Kløvedal 2021-08-30 20:10:42 +02:00 committed by GitHub
commit 27bcbdcafb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 127 deletions

View File

@ -54,7 +54,7 @@
"url-join": "^4.0.1" "url-join": "^4.0.1"
}, },
"scripts": { "scripts": {
"start": "BROWSER=none craco start", "start": "BROWSER=none PORT=1234 craco start",
"build": "craco build", "build": "craco build",
"test": "npm run lint && npm run jest -- --watchAll=false", "test": "npm run lint && npm run jest -- --watchAll=false",
"test:ci": "npm run lint && npm run jest:ci", "test:ci": "npm run lint && npm run jest:ci",

View File

@ -1,11 +1,10 @@
import React, { useCallback, useEffect } from 'react' import React, { useCallback } from 'react'
import { useQuery, gql } from '@apollo/client' import { useQuery, gql } from '@apollo/client'
import AlbumGallery from '../../components/albumGallery/AlbumGallery' import AlbumGallery from '../../components/albumGallery/AlbumGallery'
import Layout from '../../components/layout/Layout' import Layout from '../../components/layout/Layout'
import useURLParameters from '../../hooks/useURLParameters' import useURLParameters from '../../hooks/useURLParameters'
import useScrollPagination from '../../hooks/useScrollPagination' import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../../components/PaginateLoader' import PaginateLoader from '../../components/PaginateLoader'
import LazyLoad from '../../helpers/LazyLoad'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { albumQuery, albumQueryVariables } from './__generated__/albumQuery' import { albumQuery, albumQueryVariables } from './__generated__/albumQuery'
import useOrderingParams from '../../hooks/useOrderingParams' import useOrderingParams from '../../hooks/useOrderingParams'
@ -125,17 +124,6 @@ function AlbumPage({ match }: AlbumPageProps) {
[setOnlyFavorites, refetch] [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 <div>Error</div> if (error) return <div>Error</div>
return ( return (

View File

@ -1,8 +1,7 @@
import React, { useEffect } from 'react' import React from 'react'
import AlbumBoxes from '../../components/albumGallery/AlbumBoxes' import AlbumBoxes from '../../components/albumGallery/AlbumBoxes'
import Layout from '../../components/layout/Layout' import Layout from '../../components/layout/Layout'
import { useQuery, gql } from '@apollo/client' import { useQuery, gql } from '@apollo/client'
import LazyLoad from '../../helpers/LazyLoad'
import { getMyAlbums } from './__generated__/getMyAlbums' import { getMyAlbums } from './__generated__/getMyAlbums'
const getAlbumsQuery = gql` const getAlbumsQuery = gql`
@ -20,15 +19,7 @@ const getAlbumsQuery = gql`
` `
const AlbumsPage = () => { const AlbumsPage = () => {
const { loading, error, data } = useQuery<getMyAlbums>(getAlbumsQuery) const { error, data } = useQuery<getMyAlbums>(getAlbumsQuery)
useEffect(() => {
return () => LazyLoad.disconnect()
}, [])
useEffect(() => {
!loading && LazyLoad.loadImages(document.querySelectorAll('img[data-src]'))
}, [loading])
return ( return (
<Layout title="Albums"> <Layout title="Albums">

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ProtectedImage } from './ProtectedMedia' import { ProtectedImage } from './ProtectedMedia'
import { MediaType } from '../../__generated__/globalTypes' import { MediaType } from '../../__generated__/globalTypes'
@ -14,12 +14,11 @@ const MediaContainer = styled.div`
overflow: hidden; overflow: hidden;
` `
const StyledPhoto = styled(ProtectedImage)<{ loaded: boolean }>` const StyledPhoto = styled(ProtectedImage)`
height: 200px; height: 200px;
min-width: 100%; min-width: 100%;
position: relative; position: relative;
object-fit: cover; object-fit: cover;
opacity: ${({ loaded }) => (loaded ? 1 : 0)};
transition: opacity 300ms; transition: opacity 300ms;
` `
@ -29,14 +28,7 @@ type LazyPhotoProps = {
} }
const LazyPhoto = (photoProps: LazyPhotoProps) => { const LazyPhoto = (photoProps: LazyPhotoProps) => {
const [loaded, setLoaded] = useState(false) return <StyledPhoto {...photoProps} lazyLoading />
const onLoad = useCallback(e => {
!e.target.dataset.src && setLoaded(true)
}, [])
return (
<StyledPhoto {...photoProps} lazyLoading loaded={loaded} onLoad={onLoad} />
)
} }
const PhotoOverlay = styled.div<{ active: boolean }>` const PhotoOverlay = styled.div<{ active: boolean }>`

View File

@ -1,7 +1,11 @@
import classNames from 'classnames'
import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react' import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react'
import { useRef } from 'react'
import { useState } from 'react'
import { useEffect } from 'react'
import { isNil } from '../../helpers/utils' import { isNil } from '../../helpers/utils'
const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype const isNativeLazyLoadSupported = 'loading' in document.createElement('img')
const placeholder = const placeholder =
'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
@ -27,7 +31,6 @@ export interface ProtectedImageProps
src?: string src?: string
key?: string key?: string
lazyLoading?: boolean lazyLoading?: boolean
loaded?: boolean
} }
/** /**
@ -38,39 +41,81 @@ export interface ProtectedImageProps
*/ */
export const ProtectedImage = ({ export const ProtectedImage = ({
src, src,
key,
lazyLoading, lazyLoading,
loaded,
...props ...props
}: ProtectedImageProps) => { }: ProtectedImageProps) => {
const lazyLoadProps: { 'data-src'?: string; loading?: 'lazy' | 'eager' } = {} const url = getProtectedUrl(src) || placeholder
if (!isNativeLazyLoadSupported && lazyLoading) {
lazyLoadProps['data-src'] = getProtectedUrl(src)
}
if (isNativeLazyLoadSupported && lazyLoading) {
lazyLoadProps.loading = 'lazy'
}
const imgSrc: string =
lazyLoading && !isNativeLazyLoadSupported
? placeholder
: getProtectedUrl(src) || placeholder
const loadedProp =
loaded !== undefined ? { loaded: loaded.toString() } : undefined
if (!lazyLoading) {
return ( return (
<img <img {...props} src={url} loading="eager" crossOrigin="use-credentials" />
key={key}
{...props}
{...lazyLoadProps}
{...loadedProp}
src={imgSrc}
crossOrigin="use-credentials"
/>
) )
}
if (!isNativeLazyLoadSupported) {
return <FallbackLazyloadedImage src={url} {...props} />
}
// load with native lazy loading
return (
<img {...props} src={url} loading="lazy" crossOrigin="use-credentials" />
)
}
interface FallbackLazyloadedImageProps
extends Omit<
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
'src'
> {
src?: string
}
const FallbackLazyloadedImage = ({
src,
...props
}: FallbackLazyloadedImageProps) => {
const [inView, setInView] = useState(false)
const imgRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const imgElm = imgRef.current
if (isNil(imgElm) || inView) return
if (window.IntersectionObserver === undefined) {
setInView(true)
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 <img {...props} src={src} crossOrigin="use-credentials" />
} else {
return (
<div
ref={imgRef}
className={classNames(props.className, 'bg-[#eee]')}
></div>
)
}
} }
export interface ProtectedVideoProps_Media { export interface ProtectedVideoProps_Media {

View File

@ -7,7 +7,6 @@ import useURLParameters from '../../hooks/useURLParameters'
import { FavoritesCheckbox } from '../album/AlbumFilter' import { FavoritesCheckbox } from '../album/AlbumFilter'
import useScrollPagination from '../../hooks/useScrollPagination' import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../PaginateLoader' import PaginateLoader from '../PaginateLoader'
import LazyLoad from '../../helpers/LazyLoad'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
myTimeline, myTimeline,
@ -141,14 +140,6 @@ const TimelineGallery = () => {
}) })
}, [onlyFavorites]) }, [onlyFavorites])
useEffect(() => {
!loading && LazyLoad.loadImages(document.querySelectorAll('img[data-src]'))
}, [finishedLoadingMore, onlyFavorites, loading])
useEffect(() => {
return () => LazyLoad.disconnect()
}, [])
if (error) { if (error) {
return <div>{error.message}</div> return <div>{error.message}</div>
} }

View File

@ -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<Element>) {
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()