Lazy load images
This commit is contained in:
@@ -4,7 +4,6 @@ import Animated, {
|
|||||||
type AnimatedRef,
|
type AnimatedRef,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {Image} from 'expo-image'
|
|
||||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||||
import {utils} from '@bsky.app/alf'
|
import {utils} from '@bsky.app/alf'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
@@ -13,6 +12,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {type Dimensions} from '#/lib/media/types'
|
import {type Dimensions} from '#/lib/media/types'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
|
import {LazyImage} from '#/view/com/util/images/LazyImage'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
|
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
@@ -111,7 +111,7 @@ export function AutoSizedImage({
|
|||||||
|
|
||||||
const contents = (
|
const contents = (
|
||||||
<Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
<Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
||||||
<Image
|
<LazyImage
|
||||||
contentFit={isContain ? 'contain' : 'cover'}
|
contentFit={isContain ? 'contain' : 'cover'}
|
||||||
style={[a.w_full, a.h_full]}
|
style={[a.w_full, a.h_full]}
|
||||||
source={image.thumb}
|
source={image.thumb}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {type AnimatedRef} from 'react-native-reanimated'
|
import {type AnimatedRef} from 'react-native-reanimated'
|
||||||
import {Image, type ImageStyle} from 'expo-image'
|
import {type ImageStyle} from 'expo-image'
|
||||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||||
import {utils} from '@bsky.app/alf'
|
import {utils} from '@bsky.app/alf'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
@@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {type Dimensions} from '#/lib/media/types'
|
import {type Dimensions} from '#/lib/media/types'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
|
import {LazyImage} from '#/view/com/util/images/LazyImage'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
@@ -74,7 +75,7 @@ export function GalleryItem({
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={image.alt || _(msg`Image`)}
|
accessibilityLabel={image.alt || _(msg`Image`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<Image
|
<LazyImage
|
||||||
source={{uri: image.thumb}}
|
source={{uri: image.thumb}}
|
||||||
style={[a.flex_1]}
|
style={[a.flex_1]}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
@@ -82,6 +83,7 @@ export function GalleryItem({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
onLoad={e => {
|
onLoad={e => {
|
||||||
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
thumbDimsRef.current[index] = {
|
thumbDimsRef.current[index] = {
|
||||||
width: e.source.width,
|
width: e.source.width,
|
||||||
height: e.source.height,
|
height: e.source.height,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {Image, type ImageProps} from 'expo-image'
|
||||||
|
|
||||||
|
export type LazyImageProps = ImageProps & {
|
||||||
|
onLoad?: (e: {source: {width: number; height: number}}) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native implementation - just uses expo-image directly.
|
||||||
|
* The web version adds loading="lazy" for browser-native lazy loading.
|
||||||
|
*/
|
||||||
|
export function LazyImage(props: LazyImageProps) {
|
||||||
|
return <Image accessibilityIgnoresInvertColors {...props} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {type StyleProp} from 'react-native'
|
||||||
|
import {type ImageStyle} from 'expo-image'
|
||||||
|
|
||||||
|
import {type LazyImageProps} from '#/view/com/util/images/LazyImage'
|
||||||
|
|
||||||
|
function getUri(source: LazyImageProps['source']): string | undefined {
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
if (source && typeof source === 'object' && 'uri' in source) {
|
||||||
|
return source.uri
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleToCSS(style: StyleProp<ImageStyle>): React.CSSProperties {
|
||||||
|
if (!style) return {}
|
||||||
|
const flat = Array.isArray(style)
|
||||||
|
? Object.assign({}, ...style.filter(Boolean))
|
||||||
|
: style
|
||||||
|
const css: React.CSSProperties = {}
|
||||||
|
if (flat.width !== undefined) css.width = flat.width
|
||||||
|
if (flat.height !== undefined) css.height = flat.height
|
||||||
|
if (flat.flex !== undefined) css.flex = flat.flex
|
||||||
|
return css
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web implementation - uses native img with loading="lazy" for
|
||||||
|
* browser-native lazy loading to improve Lighthouse scores.
|
||||||
|
*/
|
||||||
|
export function LazyImage({
|
||||||
|
source,
|
||||||
|
style,
|
||||||
|
accessibilityLabel,
|
||||||
|
onLoad,
|
||||||
|
contentFit = 'cover',
|
||||||
|
}: LazyImageProps) {
|
||||||
|
const uri = getUri(source)
|
||||||
|
|
||||||
|
if (!uri) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={uri}
|
||||||
|
alt={accessibilityLabel}
|
||||||
|
loading="lazy"
|
||||||
|
style={{
|
||||||
|
...styleToCSS(style),
|
||||||
|
objectFit: contentFit,
|
||||||
|
}}
|
||||||
|
onLoad={e => {
|
||||||
|
const img = e.currentTarget
|
||||||
|
onLoad?.({
|
||||||
|
source: {
|
||||||
|
width: img.naturalWidth,
|
||||||
|
height: img.naturalHeight,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user