Image performance improvements (#126)

* Switch out most images for FastImage

* Add image loading placeholders

* Fix tests
This commit is contained in:
Paul Frazee
2023-01-31 17:01:02 -06:00
committed by GitHub
parent 0ce4f57e0b
commit 5ccdec1e4f
17 changed files with 182 additions and 186 deletions
+3 -6
View File
@@ -8,7 +8,7 @@ import {
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {BlurView} from '@react-native-community/blur'
import LinearGradient from 'react-native-linear-gradient'
import {AutoSizedImage} from '../util/images/AutoSizedImage'
import {Image} from '../util/images/Image'
import {Text} from '../util/text/Text'
import {s, gradients} from '../../lib/styles'
import {usePalette} from '../../lib/hooks/usePalette'
@@ -38,10 +38,7 @@ export const ExternalEmbed = ({
<ActivityIndicator size="large" style={styles.spinner} />
</View>
) : link.localThumb ? (
<AutoSizedImage
uri={link.localThumb.path}
containerStyle={styles.image}
/>
<Image uri={link.localThumb.path} style={styles.image} />
) : (
<LinearGradient
colors={[gradients.blueDark.start, gradients.blueDark.end]}
@@ -98,7 +95,7 @@ const styles = StyleSheet.create({
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
width: '100%',
height: 200,
maxHeight: 200,
},
imageFallback: {
height: 160,
+3 -2
View File
@@ -1,6 +1,7 @@
import React, {useCallback} from 'react'
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import FastImage from 'react-native-fast-image'
import {colors} from '../../lib/styles'
export const SelectedPhoto = ({
@@ -42,7 +43,7 @@ export const SelectedPhoto = ({
/>
</TouchableOpacity>
<Image
<FastImage
testID="selectedPhotoImage"
style={[styles.image, imageStyle]}
source={{uri: item}}
+14 -7
View File
@@ -1,10 +1,16 @@
import React from 'react'
import {StyleSheet, StyleProp, View, ViewStyle, Image} from 'react-native'
import {
StyleSheet,
StyleProp,
View,
ViewStyle,
Image as RNImage,
} from 'react-native'
import {AppBskyEmbedImages, AppBskyEmbedExternal} from '@atproto/api'
import LinearGradient from 'react-native-linear-gradient'
import {Link} from '../util/Link'
import {Text} from './text/Text'
import {AutoSizedImage} from './images/AutoSizedImage'
import {Image} from './images/Image'
import {ImageLayoutGrid} from './images/ImageLayoutGrid'
import {ImagesLightbox} from '../../../state/models/shell-ui'
import {useStores} from '../../../state'
@@ -37,11 +43,11 @@ export function PostEmbeds({
}
const onPressIn = (index: number) => {
const firstImageToShow = uris[index]
Image.prefetch(firstImageToShow)
RNImage.prefetch(firstImageToShow)
uris.forEach(uri => {
if (firstImageToShow !== uri) {
// First image already prefeched above
Image.prefetch(uri)
RNImage.prefetch(uri)
}
})
}
@@ -85,12 +91,12 @@ export function PostEmbeds({
} else {
return (
<View style={[styles.imagesContainer, style]}>
<AutoSizedImage
<Image
uri={embed.images[0].thumb}
onPress={() => openLightbox(0)}
onLongPress={() => onLongPress(0)}
onPressIn={() => onPressIn(0)}
containerStyle={styles.singleImage}
style={styles.singleImage}
/>
</View>
)
@@ -105,7 +111,7 @@ export function PostEmbeds({
href={link.uri}
noFeedback>
{link.thumb ? (
<AutoSizedImage uri={link.thumb} containerStyle={styles.extImage} />
<Image uri={link.thumb} style={styles.extImage} />
) : (
<LinearGradient
colors={[gradients.blueDark.start, gradients.blueDark.end]}
@@ -145,6 +151,7 @@ const styles = StyleSheet.create({
},
singleImage: {
borderRadius: 8,
maxHeight: 500,
},
extOuter: {
borderWidth: 1,
+9 -5
View File
@@ -1,7 +1,8 @@
import React from 'react'
import {Image, StyleSheet, View} from 'react-native'
import {StyleSheet, View} from 'react-native'
import Svg, {Circle, Text, Defs, LinearGradient, Stop} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import FastImage from 'react-native-fast-image'
import {
openCamera,
openCropper,
@@ -102,13 +103,16 @@ export function UserAvatar({
bottomOffset={-10}
menuWidth={170}>
{avatar ? (
<Image
<FastImage
style={{
width: size,
height: size,
borderRadius: Math.floor(size / 2),
}}
source={{uri: avatar}}
source={{
uri: avatar,
priority: FastImage.priority.high,
}}
/>
) : (
renderSvg(size, initials)
@@ -122,10 +126,10 @@ export function UserAvatar({
</View>
</DropdownButton>
) : avatar ? (
<Image
<FastImage
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
resizeMode="stretch"
source={{uri: avatar}}
source={{uri: avatar, priority: FastImage.priority.high}}
/>
) : (
renderSvg(size, initials)
+4 -3
View File
@@ -1,8 +1,9 @@
import React from 'react'
import {StyleSheet, View, Image} from 'react-native'
import {StyleSheet, View} from 'react-native'
import Svg, {Rect, Defs, LinearGradient, Stop} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Image as PickedImage} from 'react-native-image-crop-picker'
import FastImage from 'react-native-fast-image'
import {colors, gradients} from '../../lib/styles'
import {
openCamera,
@@ -95,7 +96,7 @@ export function UserBanner({
bottomOffset={-10}
menuWidth={170}>
{banner ? (
<Image style={styles.bannerImage} source={{uri: banner}} />
<FastImage style={styles.bannerImage} source={{uri: banner}} />
) : (
renderSvg()
)}
@@ -108,7 +109,7 @@ export function UserBanner({
</View>
</DropdownButton>
) : banner ? (
<Image
<FastImage
style={styles.bannerImage}
resizeMode="cover"
source={{uri: banner}}
-126
View File
@@ -1,126 +0,0 @@
import React, {useState, useEffect} from 'react'
import {
Image,
ImageStyle,
LayoutChangeEvent,
StyleProp,
StyleSheet,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {Text} from '../text/Text'
import {useTheme} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'
import {DELAY_PRESS_IN} from './constants'
const MAX_HEIGHT = 300
interface Dim {
width: number
height: number
}
export function AutoSizedImage({
uri,
onPress,
onLongPress,
onPressIn,
style,
containerStyle,
}: {
uri: string
onPress?: () => void
style?: StyleProp<ImageStyle>
containerStyle?: StyleProp<ViewStyle>
}) {
const theme = useTheme()
const errPal = usePalette('error')
const [error, setError] = useState<string | undefined>('')
const [imgInfo, setImgInfo] = useState<Dim | undefined>()
const [containerInfo, setContainerInfo] = useState<Dim | undefined>()
useEffect(() => {
let aborted = false
if (!imgInfo) {
Image.getSize(
uri,
(width: number, height: number) => {
if (!aborted) {
setImgInfo({width, height})
}
},
(err: any) => {
if (!aborted) {
setError(String(err))
}
},
)
}
return () => {
aborted = true
}
}, [uri, imgInfo])
const onLayout = (evt: LayoutChangeEvent) => {
setContainerInfo({
width: evt.nativeEvent.layout.width,
height: evt.nativeEvent.layout.height,
})
}
let calculatedStyle: StyleProp<ViewStyle> | undefined
if (imgInfo && containerInfo) {
// imgInfo.height / imgInfo.width = x / containerInfo.width
// x = imgInfo.height / imgInfo.width * containerInfo.width
calculatedStyle = {
height: Math.min(
MAX_HEIGHT,
(imgInfo.height / imgInfo.width) * containerInfo.width,
),
}
}
return (
<View style={style}>
<TouchableOpacity
onPress={onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
delayPressIn={DELAY_PRESS_IN}>
{error ? (
<View style={[styles.errorContainer, errPal.view, containerStyle]}>
<Text style={errPal.text}>{error}</Text>
</View>
) : calculatedStyle ? (
<View style={[styles.container, containerStyle]}>
<Image style={calculatedStyle} source={{uri}} />
</View>
) : (
<View
style={[
style,
styles.placeholder,
{backgroundColor: theme.palette.default.backgroundLight},
]}
onLayout={onLayout}
/>
)}
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
placeholder: {
width: '100%',
aspectRatio: 1,
},
errorContainer: {
paddingHorizontal: 12,
paddingVertical: 8,
},
container: {
overflow: 'hidden',
},
})
+40
View File
@@ -0,0 +1,40 @@
import React from 'react'
import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'
import FastImage from 'react-native-fast-image'
import {DELAY_PRESS_IN} from './constants'
import {LOADING} from '../../../lib/assets'
export function Image({
uri,
onPress,
onLongPress,
onPressIn,
style,
}: {
uri: string
onPress?: () => void
onLongPress?: () => void
onPressIn?: () => void
style?: StyleProp<ViewStyle>
}) {
return (
<TouchableOpacity
onPress={onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
delayPressIn={DELAY_PRESS_IN}
style={[styles.container, style]}>
<FastImage style={styles.image} source={{uri}} defaultSource={LOADING} />
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
},
image: {
width: '100%',
aspectRatio: 1,
},
})
+2 -2
View File
@@ -1,12 +1,12 @@
import React from 'react'
import {
Image,
StyleProp,
StyleSheet,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native'
import FastImage from 'react-native-fast-image'
export function ImageHorzList({
uris,
@@ -21,7 +21,7 @@ export function ImageHorzList({
<View style={[styles.flexRow, style]}>
{uris.map((uri, i) => (
<TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}>
<Image source={{uri}} style={styles.image} />
<FastImage source={{uri}} style={styles.image} />
</TouchableWithoutFeedback>
))}
</View>
+49 -13
View File
@@ -1,7 +1,5 @@
import React from 'react'
import {
Image,
ImageStyle,
LayoutChangeEvent,
StyleProp,
StyleSheet,
@@ -9,6 +7,8 @@ import {
View,
ViewStyle,
} from 'react-native'
import FastImage, {ImageStyle as FastImageStyle} from 'react-native-fast-image'
import {LOADING} from '../../../lib/assets'
import {DELAY_PRESS_IN} from './constants'
interface Dim {
@@ -73,7 +73,7 @@ function ImageLayoutGridInner({
onPressIn?: (index: number) => void
containerInfo: Dim
}) {
const size1 = React.useMemo<ImageStyle>(() => {
const size1 = React.useMemo<StyleProp<FastImageStyle>>(() => {
if (type === 'three') {
const size = (containerInfo.width - 10) / 3
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
@@ -82,7 +82,7 @@ function ImageLayoutGridInner({
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
}
}, [type, containerInfo])
const size2 = React.useMemo<ImageStyle>(() => {
const size2 = React.useMemo<StyleProp<FastImageStyle>>(() => {
if (type === 'three') {
const size = ((containerInfo.width - 10) / 3) * 2 + 5
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
@@ -100,7 +100,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress(0)}>
<Image source={{uri: uris[0]}} style={size1} />
<FastImage
source={{uri: uris[0]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
<View style={styles.wSpace} />
<TouchableOpacity
@@ -108,7 +112,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress(1)}>
<Image source={{uri: uris[1]}} style={size1} />
<FastImage
source={{uri: uris[1]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
</View>
)
@@ -121,7 +129,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress(0)}>
<Image source={{uri: uris[0]}} style={size2} />
<FastImage
source={{uri: uris[0]}}
style={size2}
defaultSource={LOADING}
/>
</TouchableOpacity>
<View style={styles.wSpace} />
<View>
@@ -130,7 +142,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress(1)}>
<Image source={{uri: uris[1]}} style={size1} />
<FastImage
source={{uri: uris[1]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
@@ -138,7 +154,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(2)}
onPressIn={() => onPressIn?.(2)}
onLongPress={() => onLongPress(2)}>
<Image source={{uri: uris[2]}} style={size1} />
<FastImage
source={{uri: uris[2]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
</View>
</View>
@@ -153,7 +173,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(0)}
onPressIn={() => onPressIn?.(0)}
onLongPress={() => onLongPress(0)}>
<Image source={{uri: uris[0]}} style={size1} />
<FastImage
source={{uri: uris[0]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
@@ -161,7 +185,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(2)}
onPressIn={() => onPressIn?.(2)}
onLongPress={() => onLongPress(2)}>
<Image source={{uri: uris[2]}} style={size1} />
<FastImage
source={{uri: uris[2]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
</View>
<View style={styles.wSpace} />
@@ -171,7 +199,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(1)}
onPressIn={() => onPressIn?.(1)}
onLongPress={() => onLongPress(1)}>
<Image source={{uri: uris[1]}} style={size1} />
<FastImage
source={{uri: uris[1]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
<View style={styles.hSpace} />
<TouchableOpacity
@@ -179,7 +211,11 @@ function ImageLayoutGridInner({
onPress={() => onPress?.(3)}
onPressIn={() => onPressIn?.(3)}
onLongPress={() => onLongPress(3)}>
<Image source={{uri: uris[3]}} style={size1} />
<FastImage
source={{uri: uris[3]}}
style={size1}
defaultSource={LOADING}
/>
</TouchableOpacity>
</View>
</View>
+5 -4
View File
@@ -1,5 +1,6 @@
import {ImageSourcePropType} from 'react-native'
import {ImageRequireSource} from 'react-native'
export const DEF_AVATAR: ImageSourcePropType = require('../../../public/img/default-avatar.jpg')
export const TABS_EXPLAINER: ImageSourcePropType = require('../../../public/img/tabs-explainer.jpg')
export const CLOUD_SPLASH: ImageSourcePropType = require('../../../public/img/cloud-splash.png')
export const DEF_AVATAR: ImageRequireSource = require('../../../public/img/default-avatar.jpg')
export const TABS_EXPLAINER: ImageRequireSource = require('../../../public/img/tabs-explainer.jpg')
export const CLOUD_SPLASH: ImageRequireSource = require('../../../public/img/cloud-splash.png')
export const LOADING: ImageRequireSource = require('../../../public/img/loading.png')
+9 -4
View File
@@ -1,7 +1,12 @@
import {ImageSourcePropType} from 'react-native'
import {ImageRequireSource} from 'react-native'
export const DEF_AVATAR: ImageSourcePropType = {uri: '/img/default-avatar.jpg'}
export const TABS_EXPLAINER: ImageSourcePropType = {
// @ts-ignore we need to pretend -prf
export const DEF_AVATAR: ImageRequireSource = {uri: '/img/default-avatar.jpg'}
// @ts-ignore we need to pretend -prf
export const TABS_EXPLAINER: ImageRequireSource = {
uri: '/img/tabs-explainer.jpg',
}
export const CLOUD_SPLASH: ImageSourcePropType = {uri: '/img/cloud-splash.png'}
// @ts-ignore we need to pretend -prf
export const CLOUD_SPLASH: ImageRequireSource = {uri: '/img/cloud-splash.png'}
// @ts-ignore we need to pretend -prf
export const LOADING: ImageRequireSource = {uri: '/img/loading.png'}
+3 -8
View File
@@ -1,11 +1,6 @@
import React, {useState} from 'react'
import {
Image,
SafeAreaView,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
import FastImage, {Source as FISource} from 'react-native-fast-image'
import {observer} from 'mobx-react-lite'
import {Signin} from '../com/login/Signin'
import {CreateAccount} from '../com/login/CreateAccount'
@@ -64,7 +59,7 @@ export const Login = observer(() => {
if (screenState === ScreenState.S_SigninOrCreateAccount) {
return (
<View style={styles.container}>
<Image source={CLOUD_SPLASH} style={styles.bgImg} />
<FastImage source={CLOUD_SPLASH as FISource} style={styles.bgImg} />
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
<SigninOrCreateAccount