diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 92210e55e7..63089fbf7f 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -4,7 +4,6 @@ import Animated, { type AnimatedRef, useAnimatedRef, } from 'react-native-reanimated' -import {Image} from 'expo-image' import {type AppBskyEmbedImages} from '@atproto/api' import {utils} from '@bsky.app/alf' import {msg} from '@lingui/macro' @@ -13,6 +12,7 @@ import {useLingui} from '@lingui/react' import {type Dimensions} from '#/lib/media/types' import {isNative} from '#/platform/detection' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {LazyImage} from '#/view/com/util/images/LazyImage' import {atoms as a, useTheme} from '#/alf' import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' import {MediaInsetBorder} from '#/components/MediaInsetBorder' @@ -111,7 +111,7 @@ export function AutoSizedImage({ const contents = ( - - { + // eslint-disable-next-line react-compiler/react-compiler thumbDimsRef.current[index] = { width: e.source.width, height: e.source.height, diff --git a/src/view/com/util/images/LazyImage.tsx b/src/view/com/util/images/LazyImage.tsx new file mode 100644 index 0000000000..95738c6dac --- /dev/null +++ b/src/view/com/util/images/LazyImage.tsx @@ -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 +} diff --git a/src/view/com/util/images/LazyImage.web.tsx b/src/view/com/util/images/LazyImage.web.tsx new file mode 100644 index 0000000000..4740eee88a --- /dev/null +++ b/src/view/com/util/images/LazyImage.web.tsx @@ -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): 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 ( + {accessibilityLabel} { + const img = e.currentTarget + onLoad?.({ + source: { + width: img.naturalWidth, + height: img.naturalHeight, + }, + }) + }} + /> + ) +}