Flatten seven preference providers into one

Each of these preferences was its own Provider rendering a state context and a
setter context - three fibers apiece, all sitting between the app root and every
screen.

That depth is not free during scrolling. React calls
propagateParentContextChanges unconditionally on every bailout, and the walk
iterates every fiber on the return path to the root. A scrolling feed bails out
on most of its mounted cells on every list update, so the provider stack gets
multiplied by (list renders per second x mounted cells).

Merging these into a single state context and a single setter context removes 18
of the 36 fibers. The trade is a wider re-render when a preference changes, which
only happens from a settings screen.

Measured on a Galaxy A16 (Hermes CPU profile, production bundle, 10-swipe scroll,
3 runs, medians): JS busy 5667ms -> 5389ms (-4.9%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tomek Zawadzki
2026-08-27 08:17:35 +02:00
parent c4c999ff4f
commit 393f9a3e2f
10 changed files with 135 additions and 390 deletions
-35
View File
@@ -1091,21 +1091,6 @@
"count": 1
}
},
"src/state/preferences/alt-text-required.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/preferences/autoplay.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/preferences/disable-haptics.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/preferences/external-embeds-prefs.tsx": {
"typescript/no-floating-promises": {
"count": 1
@@ -1116,11 +1101,6 @@
"count": 1
}
},
"src/state/preferences/in-app-browser.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/preferences/kawaii.tsx": {
"typescript/no-floating-promises": {
"count": 1
@@ -1131,26 +1111,11 @@
"count": 1
}
},
"src/state/preferences/large-alt-badge.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/preferences/subtitles.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/preferences/trending.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/preferences/used-starter-packs.tsx": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/queries/activity-subscriptions.ts": {
"typescript/no-floating-promises": {
"count": 1
+4 -52
View File
@@ -1,58 +1,10 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import * as persisted from '#/state/persisted'
type StateContext = persisted.Schema['requireAltTextEnabled']
type SetContext = (v: persisted.Schema['requireAltTextEnabled']) => void
const stateContext = createContext<StateContext>(
persisted.defaults.requireAltTextEnabled,
)
stateContext.displayName = 'AltTextRequiredStateContext'
const setContext = createContext<SetContext>(
(_: persisted.Schema['requireAltTextEnabled']) => {},
)
setContext.displayName = 'AltTextRequiredSetContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = useState(persisted.get('requireAltTextEnabled'))
const setStateWrapped = useCallback(
(requireAltTextEnabled: persisted.Schema['requireAltTextEnabled']) => {
setState(requireAltTextEnabled)
persisted.write('requireAltTextEnabled', requireAltTextEnabled)
},
[setState],
)
useEffect(() => {
return persisted.onUpdate(
'requireAltTextEnabled',
nextRequireAltTextEnabled => {
setState(nextRequireAltTextEnabled)
},
)
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
import {usePref, useSetPref} from './simple-prefs'
/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */
export function useRequireAltTextEnabled() {
return useContext(stateContext)
return usePref('requireAltTextEnabled')
}
export function useSetRequireAltTextEnabled() {
return useContext(setContext)
return useSetPref('requireAltTextEnabled')
}
+7 -45
View File
@@ -1,48 +1,10 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import {usePref, useSetPref} from './simple-prefs'
import * as persisted from '#/state/persisted'
type StateContext = boolean
type SetContext = (v: boolean) => void
const stateContext = createContext<StateContext>(
Boolean(persisted.defaults.disableAutoplay),
)
stateContext.displayName = 'AutoplayStateContext'
const setContext = createContext<SetContext>((_: boolean) => {})
setContext.displayName = 'AutoplaySetContext'
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = useState(Boolean(persisted.get('disableAutoplay')))
const setStateWrapped = useCallback(
(autoplayDisabled: persisted.Schema['disableAutoplay']) => {
setState(Boolean(autoplayDisabled))
persisted.write('disableAutoplay', autoplayDisabled)
},
[setState],
)
useEffect(() => {
return persisted.onUpdate('disableAutoplay', nextDisableAutoplay => {
setState(Boolean(nextDisableAutoplay))
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */
export function useAutoplayDisabled() {
return Boolean(usePref('disableAutoplay'))
}
export const useAutoplayDisabled = () => useContext(stateContext)
export const useSetAutoplayDisabled = () => useContext(setContext)
export function useSetAutoplayDisabled() {
return useSetPref('disableAutoplay')
}
+7 -45
View File
@@ -1,48 +1,10 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import {usePref, useSetPref} from './simple-prefs'
import * as persisted from '#/state/persisted'
type StateContext = boolean
type SetContext = (v: boolean) => void
const stateContext = createContext<StateContext>(
Boolean(persisted.defaults.disableHaptics),
)
stateContext.displayName = 'DisableHapticsStateContext'
const setContext = createContext<SetContext>((_: boolean) => {})
setContext.displayName = 'DisableHapticsSetContext'
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = useState(Boolean(persisted.get('disableHaptics')))
const setStateWrapped = useCallback(
(hapticsEnabled: persisted.Schema['disableHaptics']) => {
setState(Boolean(hapticsEnabled))
persisted.write('disableHaptics', hapticsEnabled)
},
[setState],
)
useEffect(() => {
return persisted.onUpdate('disableHaptics', nextDisableHaptics => {
setState(Boolean(nextDisableHaptics))
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */
export function useHapticsDisabled() {
return Boolean(usePref('disableHaptics'))
}
export const useHapticsDisabled = () => useContext(stateContext)
export const useSetHapticsDisabled = () => useContext(setContext)
export function useSetHapticsDisabled() {
return useSetPref('disableHaptics')
}
+4 -49
View File
@@ -1,55 +1,10 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import * as persisted from '#/state/persisted'
type StateContext = persisted.Schema['useInAppBrowser']
type SetContext = (v: persisted.Schema['useInAppBrowser']) => void
const stateContext = createContext<StateContext>(
persisted.defaults.useInAppBrowser,
)
stateContext.displayName = 'InAppBrowserStateContext'
const setContext = createContext<SetContext>(
(_: persisted.Schema['useInAppBrowser']) => {},
)
setContext.displayName = 'InAppBrowserSetContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = useState(persisted.get('useInAppBrowser'))
const setStateWrapped = useCallback(
(inAppBrowser: persisted.Schema['useInAppBrowser']) => {
setState(inAppBrowser)
persisted.write('useInAppBrowser', inAppBrowser)
},
[setState],
)
useEffect(() => {
return persisted.onUpdate('useInAppBrowser', nextUseInAppBrowser => {
setState(nextUseInAppBrowser)
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
import {usePref, useSetPref} from './simple-prefs'
/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */
export function useInAppBrowser() {
return useContext(stateContext)
return usePref('useInAppBrowser')
}
export function useSetInAppBrowser() {
return useContext(setContext)
return useSetPref('useInAppBrowser')
}
+10 -28
View File
@@ -1,15 +1,9 @@
import {Provider as AltTextRequiredProvider} from './alt-text-required'
import {Provider as AutoplayProvider} from './autoplay'
import {Provider as DisableHapticsProvider} from './disable-haptics'
import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs'
import {Provider as HiddenPostsProvider} from './hidden-posts'
import {Provider as InAppBrowserProvider} from './in-app-browser'
import {Provider as KawaiiProvider} from './kawaii'
import {Provider as LanguagesProvider} from './languages'
import {Provider as LargeAltBadgeProvider} from './large-alt-badge'
import {Provider as SubtitlesProvider} from './subtitles'
import {Provider as SimplePrefsProvider} from './simple-prefs'
import {Provider as TrendingSettingsProvider} from './trending'
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
export {
useRequireAltTextEnabled,
@@ -29,27 +23,15 @@ export {useSetSubtitlesEnabled, useSubtitlesEnabled} from './subtitles'
export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<LanguagesProvider>
<AltTextRequiredProvider>
<LargeAltBadgeProvider>
<ExternalEmbedsProvider>
<HiddenPostsProvider>
<InAppBrowserProvider>
<DisableHapticsProvider>
<AutoplayProvider>
<UsedStarterPacksProvider>
<SubtitlesProvider>
<TrendingSettingsProvider>
<KawaiiProvider>{children}</KawaiiProvider>
</TrendingSettingsProvider>
</SubtitlesProvider>
</UsedStarterPacksProvider>
</AutoplayProvider>
</DisableHapticsProvider>
</InAppBrowserProvider>
</HiddenPostsProvider>
</ExternalEmbedsProvider>
</LargeAltBadgeProvider>
</AltTextRequiredProvider>
<SimplePrefsProvider>
<ExternalEmbedsProvider>
<HiddenPostsProvider>
<TrendingSettingsProvider>
<KawaiiProvider>{children}</KawaiiProvider>
</TrendingSettingsProvider>
</HiddenPostsProvider>
</ExternalEmbedsProvider>
</SimplePrefsProvider>
</LanguagesProvider>
)
}
+4 -52
View File
@@ -1,58 +1,10 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import * as persisted from '#/state/persisted'
type StateContext = persisted.Schema['largeAltBadgeEnabled']
type SetContext = (v: persisted.Schema['largeAltBadgeEnabled']) => void
const stateContext = createContext<StateContext>(
persisted.defaults.largeAltBadgeEnabled,
)
stateContext.displayName = 'LargeAltBadgeStateContext'
const setContext = createContext<SetContext>(
(_: persisted.Schema['largeAltBadgeEnabled']) => {},
)
setContext.displayName = 'LargeAltBadgeSetContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = useState(persisted.get('largeAltBadgeEnabled'))
const setStateWrapped = useCallback(
(largeAltBadgeEnabled: persisted.Schema['largeAltBadgeEnabled']) => {
setState(largeAltBadgeEnabled)
persisted.write('largeAltBadgeEnabled', largeAltBadgeEnabled)
},
[setState],
)
useEffect(() => {
return persisted.onUpdate(
'largeAltBadgeEnabled',
nextLargeAltBadgeEnabled => {
setState(nextLargeAltBadgeEnabled)
},
)
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
import {usePref, useSetPref} from './simple-prefs'
/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */
export function useLargeAltBadgeEnabled() {
return useContext(stateContext)
return usePref('largeAltBadgeEnabled')
}
export function useSetLargeAltBadgeEnabled() {
return useContext(setContext)
return useSetPref('largeAltBadgeEnabled')
}
+85
View File
@@ -0,0 +1,85 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import * as persisted from '#/state/persisted'
/*
* Several preferences were each their own Provider, and each of those rendered a
* state context plus a setter context - three fibers apiece, all of them sitting
* between the app root and every screen.
*
* That depth is not free. React walks the full return path to the root on every
* bailout, and a scrolling list bails out on most of its cells on every update,
* so the provider stack is multiplied by (renders per second x mounted cells).
* Collapsing these into one state context and one setter context trades a wider
* re-render on preference change - which only happens from a settings screen -
* for a permanently shallower tree.
*/
const KEYS = [
'requireAltTextEnabled',
'disableAutoplay',
'disableHaptics',
'useInAppBrowser',
'largeAltBadgeEnabled',
'subtitlesEnabled',
'hasCheckedForStarterPack',
] as const
type Key = (typeof KEYS)[number]
type Values = {[K in Key]: persisted.Schema[K]}
type SetFn = <K extends Key>(key: K, value: Values[K]) => void
function readAll(): Values {
const out = {} as Values
for (const key of KEYS) {
// @ts-expect-error indexed write across the key union
out[key] = persisted.get(key)
}
return out
}
const stateContext = createContext<Values>(
Object.fromEntries(KEYS.map(k => [k, persisted.defaults[k]])) as Values,
)
stateContext.displayName = 'SimplePrefsStateContext'
const setContext = createContext<SetFn>(() => {})
setContext.displayName = 'SimplePrefsSetContext'
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = useState(readAll)
useEffect(() => {
const unsubs = KEYS.map(key =>
persisted.onUpdate(key, next => {
setState(prev => ({...prev, [key]: next}))
}),
)
return () => unsubs.forEach(unsub => unsub())
}, [])
const set = useCallback<SetFn>((key, value) => {
setState(prev => ({...prev, [key]: value}))
// @ts-expect-error indexed write across the key union
void persisted.write(key, value)
}, [])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={set}>{children}</setContext.Provider>
</stateContext.Provider>
)
}
export function usePref<K extends Key>(key: K): Values[K] {
return useContext(stateContext)[key]
}
export function useSetPref<K extends Key>(key: K): (value: Values[K]) => void {
const set = useContext(setContext)
return useCallback((value: Values[K]) => set(key, value), [set, key])
}
+7 -45
View File
@@ -1,48 +1,10 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import {usePref, useSetPref} from './simple-prefs'
import * as persisted from '#/state/persisted'
type StateContext = boolean
type SetContext = (v: boolean) => void
const stateContext = createContext<StateContext>(
Boolean(persisted.defaults.subtitlesEnabled),
)
stateContext.displayName = 'SubtitlesStateContext'
const setContext = createContext<SetContext>((_: boolean) => {})
setContext.displayName = 'SubtitlesSetContext'
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = useState(Boolean(persisted.get('subtitlesEnabled')))
const setStateWrapped = useCallback(
(subtitlesEnabled: persisted.Schema['subtitlesEnabled']) => {
setState(Boolean(subtitlesEnabled))
persisted.write('subtitlesEnabled', subtitlesEnabled)
},
[setState],
)
useEffect(() => {
return persisted.onUpdate('subtitlesEnabled', nextSubtitlesEnabled => {
setState(Boolean(nextSubtitlesEnabled))
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */
export function useSubtitlesEnabled() {
return Boolean(usePref('subtitlesEnabled'))
}
export const useSubtitlesEnabled = () => useContext(stateContext)
export const useSetSubtitlesEnabled = () => useContext(setContext)
export function useSetSubtitlesEnabled() {
return useSetPref('subtitlesEnabled')
}
+7 -39
View File
@@ -1,42 +1,10 @@
import {createContext, useContext, useEffect, useState} from 'react'
import {usePref, useSetPref} from './simple-prefs'
import * as persisted from '#/state/persisted'
type StateContext = boolean | undefined
type SetContext = (v: boolean) => void
const stateContext = createContext<StateContext>(false)
stateContext.displayName = 'UsedStarterPacksStateContext'
const setContext = createContext<SetContext>((_: boolean) => {})
setContext.displayName = 'UsedStarterPacksSetContext'
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = useState<StateContext>(() =>
persisted.get('hasCheckedForStarterPack'),
)
const setStateWrapped = (v: boolean) => {
setState(v)
persisted.write('hasCheckedForStarterPack', v)
}
useEffect(() => {
return persisted.onUpdate(
'hasCheckedForStarterPack',
nextHasCheckedForStarterPack => {
setState(nextHasCheckedForStarterPack)
},
)
}, [])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
/* Backed by the merged simple-prefs provider; see simple-prefs.tsx. */
export function useHasCheckedForStarterPack() {
return usePref('hasCheckedForStarterPack')
}
export const useHasCheckedForStarterPack = () => useContext(stateContext)
export const useSetHasCheckedForStarterPack = () => useContext(setContext)
export function useSetHasCheckedForStarterPack() {
return useSetPref('hasCheckedForStarterPack')
}