Add <DeferReveal> to defer expensive views

This commit is contained in:
Dan Abramov
2024-11-24 21:52:58 +00:00
parent b9406aa011
commit df9a0a075f
6 changed files with 202 additions and 159 deletions
+23
View File
@@ -0,0 +1,23 @@
import {ReactNode, useEffect, useState} from 'react'
export function DeferReveal({
children,
defer,
// To verify, toggle `forceReveal` between `true` and `false`.
iVerifiedThereAreNoLayoutJumps: _unused,
forceReveal,
}: {
children: ReactNode
defer: boolean
iVerifiedThereAreNoLayoutJumps: true
forceReveal?: never // DEV-only
}) {
const [isReady, setIsReady] = useState(!defer)
useEffect(() => {
if (!isReady) {
setIsReady(true)
}
}, [isReady])
const finalIsReady = forceReveal ?? isReady
return finalIsReady ? children : null
}
+73 -69
View File
@@ -8,8 +8,10 @@ import {useLingui} from '@lingui/react'
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef' import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
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 {isAndroid} from '#/platform/detection'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {DeferReveal} from '#/components/DeferReveal'
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'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -107,84 +109,86 @@ export function AutoSizedImage({
const contents = ( const contents = (
<View ref={containerRef} collapsable={false} style={{flex: 1}}> <View ref={containerRef} collapsable={false} style={{flex: 1}}>
<Image <DeferReveal defer={isAndroid} iVerifiedThereAreNoLayoutJumps>
style={[a.w_full, a.h_full]} <Image
source={image.thumb} style={[a.w_full, a.h_full]}
accessible={true} // Must set for `accessibilityLabel` to work source={image.thumb}
accessibilityIgnoresInvertColors accessible={true} // Must set for `accessibilityLabel` to work
accessibilityLabel={image.alt} accessibilityIgnoresInvertColors
accessibilityHint="" accessibilityLabel={image.alt}
onLoad={e => { accessibilityHint=""
fetchedDimsRef.current = { onLoad={e => {
width: e.source.width, fetchedDimsRef.current = {
height: e.source.height, width: e.source.width,
} height: e.source.height,
}} }
/> }}
<MediaInsetBorder /> />
<MediaInsetBorder />
{(hasAlt || isCropped) && !hideBadge ? ( {(hasAlt || isCropped) && !hideBadge ? (
<View <View
accessible={false} accessible={false}
style={[ style={[
a.absolute, a.absolute,
a.flex_row, a.flex_row,
{
bottom: a.p_xs.padding,
right: a.p_xs.padding,
gap: 3,
},
largeAlt && [
{ {
gap: 4, bottom: a.p_xs.padding,
right: a.p_xs.padding,
gap: 3,
}, },
], largeAlt && [
]}>
{isCropped && (
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{ {
padding: 3, gap: 4,
opacity: 0.8,
}, },
largeAlt && [ ],
]}>
{isCropped && (
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{ {
padding: 5, padding: 3,
opacity: 0.8,
}, },
], largeAlt && [
]}> {
<Fullscreen padding: 5,
fill={t.atoms.text_contrast_high.color} },
width={largeAlt ? 18 : 12} ],
/> ]}>
</View> <Fullscreen
)} fill={t.atoms.text_contrast_high.color}
{hasAlt && ( width={largeAlt ? 18 : 12}
<View />
style={[ </View>
a.justify_center, )}
a.rounded_xs, {hasAlt && (
t.atoms.bg_contrast_25, <View
{ style={[
padding: 3, a.justify_center,
opacity: 0.8, a.rounded_xs,
}, t.atoms.bg_contrast_25,
largeAlt && [
{ {
padding: 5, padding: 3,
opacity: 0.8,
}, },
], largeAlt && [
]}> {
<Text padding: 5,
style={[a.font_heavy, largeAlt ? a.text_xs : {fontSize: 8}]}> },
ALT ],
</Text> ]}>
</View> <Text
)} style={[a.font_heavy, largeAlt ? a.text_xs : {fontSize: 8}]}>
</View> ALT
) : null} </Text>
</View>
)}
</View>
) : null}
</DeferReveal>
</View> </View>
) )
+18 -14
View File
@@ -7,9 +7,11 @@ import {useLingui} from '@lingui/react'
import {HandleRef} from '#/lib/hooks/useHandleRef' import {HandleRef} from '#/lib/hooks/useHandleRef'
import {Dimensions} from '#/lib/media/types' import {Dimensions} from '#/lib/media/types'
import {isAndroid} from '#/platform/detection'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {DeferReveal} from '#/components/DeferReveal'
import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -70,20 +72,22 @@ export function GalleryItem({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={image.alt || _(msg`Image`)} accessibilityLabel={image.alt || _(msg`Image`)}
accessibilityHint=""> accessibilityHint="">
<Image <DeferReveal defer={isAndroid} iVerifiedThereAreNoLayoutJumps>
source={{uri: image.thumb}} <Image
style={[a.flex_1]} source={{uri: image.thumb}}
accessible={true} style={[a.flex_1]}
accessibilityLabel={image.alt} accessible={true}
accessibilityHint="" accessibilityLabel={image.alt}
accessibilityIgnoresInvertColors accessibilityHint=""
onLoad={e => { accessibilityIgnoresInvertColors
thumbDimsRef.current[index] = { onLoad={e => {
width: e.source.width, thumbDimsRef.current[index] = {
height: e.source.height, width: e.source.width,
} height: e.source.height,
}} }
/> }}
/>
</DeferReveal>
<MediaInsetBorder style={insetBorderStyle} /> <MediaInsetBorder style={insetBorderStyle} />
</Pressable> </Pressable>
{hasAlt && !hideBadges ? ( {hasAlt && !hideBadges ? (
@@ -9,12 +9,13 @@ import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
import {toNiceDomain} from '#/lib/strings/url-helpers' import {toNiceDomain} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection' import {isAndroid, isNative} from '#/platform/detection'
import {useExternalEmbedsPrefs} from '#/state/preferences' import {useExternalEmbedsPrefs} from '#/state/preferences'
import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed'
import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed'
import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {DeferReveal} from '#/components/DeferReveal'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
@@ -87,13 +88,18 @@ export const ExternalLinkEmbed = ({
: t.atoms.border_contrast_low, : t.atoms.border_contrast_low,
]}> ]}>
{imageUri && !embedPlayerParams ? ( {imageUri && !embedPlayerParams ? (
<Image <View
style={{ style={{
aspectRatio: 1.91, aspectRatio: 1.91,
}} }}>
source={{uri: imageUri}} <DeferReveal defer={isAndroid} iVerifiedThereAreNoLayoutJumps>
accessibilityIgnoresInvertColors <Image
/> style={{flex: 1}}
source={{uri: imageUri}}
accessibilityIgnoresInvertColors
/>
</DeferReveal>
</View>
) : undefined} ) : undefined}
{embedPlayerParams?.isGif ? ( {embedPlayerParams?.isGif ? (
@@ -23,9 +23,10 @@ import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
import {isNative} from '#/platform/detection' import {isAndroid, isNative} from '#/platform/detection'
import {useExternalEmbedsPrefs} from '#/state/preferences' import {useExternalEmbedsPrefs} from '#/state/preferences'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {DeferReveal} from '#/components/DeferReveal'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
import {Fill} from '#/components/Fill' import {Fill} from '#/components/Fill'
@@ -217,43 +218,45 @@ export function ExternalPlayer({
ref={viewRef} ref={viewRef}
collapsable={false} collapsable={false}
style={[aspect, a.overflow_hidden]}> style={[aspect, a.overflow_hidden]}>
{link.thumb && (!isPlayerActive || isLoading) ? ( <DeferReveal defer={isAndroid} iVerifiedThereAreNoLayoutJumps>
<> {link.thumb && (!isPlayerActive || isLoading) ? (
<Image <>
style={[a.flex_1]} <Image
source={{uri: link.thumb}} style={[a.flex_1]}
accessibilityIgnoresInvertColors source={{uri: link.thumb}}
/> accessibilityIgnoresInvertColors
/>
<Fill
style={[
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
{
opacity: 0.3,
},
]}
/>
</>
) : (
<Fill <Fill
style={[ style={[
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
{ {
backgroundColor:
t.name === 'light' ? t.palette.contrast_975 : 'black',
opacity: 0.3, opacity: 0.3,
}, },
]} ]}
/> />
</> )}
) : ( <PlaceholderOverlay
<Fill isLoading={isLoading}
style={[ isPlayerActive={isPlayerActive}
{ onPress={onPlayPress}
backgroundColor:
t.name === 'light' ? t.palette.contrast_975 : 'black',
opacity: 0.3,
},
]}
/> />
)} <Player
<PlaceholderOverlay isPlayerActive={isPlayerActive}
isLoading={isLoading} params={params}
isPlayerActive={isPlayerActive} onLoad={onLoad}
onPress={onPlayPress} />
/> </DeferReveal>
<Player
isPlayerActive={isPlayerActive}
params={params}
onLoad={onLoad}
/>
</Animated.View> </Animated.View>
</> </>
) )
+43 -40
View File
@@ -12,10 +12,11 @@ import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {EmbedPlayerParams} from '#/lib/strings/embed-player' import {EmbedPlayerParams} from '#/lib/strings/embed-player'
import {isWeb} from '#/platform/detection' import {isAndroid, isWeb} from '#/platform/detection'
import {useAutoplayDisabled} from '#/state/preferences' import {useAutoplayDisabled} from '#/state/preferences'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {DeferReveal} from '#/components/DeferReveal'
import {Fill} from '#/components/Fill' import {Fill} from '#/components/Fill'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
@@ -121,46 +122,48 @@ export function GifEmbed({
{aspectRatio: params.dimensions!.width / params.dimensions!.height}, {aspectRatio: params.dimensions!.width / params.dimensions!.height},
style, style,
]}> ]}>
<View <DeferReveal defer={isAndroid} iVerifiedThereAreNoLayoutJumps>
style={[ <View
a.absolute, style={[
/* a.absolute,
* Aspect ratio was being clipped weirdly on web -esb /*
*/ * Aspect ratio was being clipped weirdly on web -esb
{ */
top: -2, {
bottom: -2, top: -2,
left: -2, bottom: -2,
right: -2, left: -2,
}, right: -2,
]}> },
<PlaybackControls ]}>
onPress={onPress} <PlaybackControls
isPlaying={playerState.isPlaying} onPress={onPress}
isLoaded={playerState.isLoaded} isPlaying={playerState.isPlaying}
/> isLoaded={playerState.isLoaded}
<GifView
source={params.playerUri}
placeholderSource={thumb}
style={[a.flex_1]}
autoplay={!autoplayDisabled}
onPlayerStateChange={onPlayerStateChange}
ref={playerRef}
accessibilityHint={_(msg`Animated GIF`)}
accessibilityLabel={altText}
/>
{!playerState.isPlaying && (
<Fill
style={[
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
{
opacity: 0.3,
},
]}
/> />
)} <GifView
{!hideAlt && isPreferredAltText && <AltText text={altText} />} source={params.playerUri}
</View> placeholderSource={thumb}
style={[a.flex_1]}
autoplay={!autoplayDisabled}
onPlayerStateChange={onPlayerStateChange}
ref={playerRef}
accessibilityHint={_(msg`Animated GIF`)}
accessibilityLabel={altText}
/>
{!playerState.isPlaying && (
<Fill
style={[
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
{
opacity: 0.3,
},
]}
/>
)}
{!hideAlt && isPreferredAltText && <AltText text={altText} />}
</View>
</DeferReveal>
</View> </View>
) )
} }