Lazy load images

This commit is contained in:
Alex Benzer
2025-12-03 13:58:22 -08:00
parent f0c9e1ea5e
commit 9a050b126a
4 changed files with 84 additions and 4 deletions
+2 -2
View File
@@ -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}
+4 -2
View File
@@ -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,
+13
View File
@@ -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,
},
})
}}
/>
)
}