Merge pull request #484 from photoview/fix-324
Refactor image lazy loading
This commit is contained in:
commit
27bcbdcafb
|
@ -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",
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 }>`
|
||||||
|
|
|
@ -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,41 +41,83 @@ 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 {
|
||||||
__typename: 'Media'
|
__typename: 'Media'
|
||||||
id: string
|
id: string
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
Loading…
Reference in New Issue