Skip to content

Commit

Permalink
Restyle performance improvements (#220)
Browse files Browse the repository at this point in the history
  • Loading branch information
fortmarek authored Feb 23, 2023
1 parent 87d5e53 commit a1f37af
Show file tree
Hide file tree
Showing 16 changed files with 330 additions and 192 deletions.
2 changes: 1 addition & 1 deletion documentation/docs/fundamentals/breakpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ const theme = createTheme({
});
```

See the [Responsive Values](/fundamentals/responsive-values) section to see how these can be used.
See the [Responsive Values](/fundamentals/responsive-values) section to see how these can be used. Defining `breakpoints` is optional and we recommend defining it only if you plan to use them due to a performance hit (up to 10 % worse average FPS when scrolling in a list) responsive values incur.
4 changes: 0 additions & 4 deletions documentation/docs/fundamentals/defining-your-theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ const theme = createTheme({
l: 24,
xl: 40,
},
breakpoints: {
phone: 0,
tablet: 768,
},
});

export type Theme = typeof theme;
Expand Down
1 change: 0 additions & 1 deletion documentation/docs/guides/dark-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const theme = createTheme({
primaryCardText: palette.white,
secondaryCardText: palette.black,
},
breakpoints: {},
textVariants: {
body: {
fontSize: 16,
Expand Down
23 changes: 14 additions & 9 deletions src/composeRestyleFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {StyleSheet} from 'react-native';
import {StyleSheet, ViewStyle} from 'react-native';

import {
RestyleFunctionContainer,
Expand Down Expand Up @@ -46,16 +46,21 @@ const composeRestyleFunctions = <
dimensions,
}: {
theme: Theme;
dimensions: Dimensions;
dimensions: Dimensions | null;
},
): RNStyle => {
const styles = Object.keys(props).reduce(
(styleObj, propKey) => ({
...styleObj,
...funcsMap[propKey as keyof TProps](props, {theme, dimensions}),
}),
{},
);
const styles: ViewStyle = {};
const options = {theme, dimensions};
// We make the assumption that the props object won't have extra prototype keys.
// eslint-disable-next-line guard-for-in
for (const key in props) {
const mappedProps = funcsMap[key](props, options);
// eslint-disable-next-line guard-for-in
for (const mappedKey in mappedProps) {
styles[mappedKey as keyof ViewStyle] = mappedProps[mappedKey];
}
}

const {stylesheet} = StyleSheet.create({stylesheet: styles});
return stylesheet;
};
Expand Down
1 change: 0 additions & 1 deletion src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {BaseTheme} from 'types';
export const ThemeContext = React.createContext({
colors: {},
spacing: {},
breakpoints: {},
});

export const ThemeProvider = ({
Expand Down
91 changes: 83 additions & 8 deletions src/createRestyleFunction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import {BaseTheme, RestyleFunction, RNStyleProperty} from './types';
import {getResponsiveValue, StyleTransformFunction} from './responsiveHelpers';
import {getThemeValue} from './utilities';
import {
BaseTheme,
Dimensions,
ResponsiveBaseTheme,
RestyleFunction,
RNStyleProperty,
StyleTransformFunction,
} from './types';
import {getResponsiveValue} from './utilities/getResponsiveValue';

const getMemoizedMapHashKey = (
dimensions: Dimensions | null,
themeKey: string,
property: string,
value: string | number | boolean,
) => {
return `${dimensions?.height}x${dimensions?.width}-${themeKey}-${property}-${value}`;
};

type ThemeWithMemoization<Theme extends BaseTheme> = Theme & {
unsafeMemoizedMap: {[key: string]: any} | null;
};

const createRestyleFunction = <
Theme extends BaseTheme = BaseTheme,
Expand All @@ -24,14 +45,59 @@ const createRestyleFunction = <
props,
{theme, dimensions},
) => {
const value = getResponsiveValue(props[property], {
theme,
dimensions,
themeKey,
transform,
});
// We reuse the theme object for saving the memoized values.
const unsafeTheme: ThemeWithMemoization<Theme> =
theme as ThemeWithMemoization<Theme>;
if (unsafeTheme.unsafeMemoizedMap == null) {
unsafeTheme.unsafeMemoizedMap = {};
}

const memoizedMapHashKey = (() => {
if (
themeKey &&
property &&
props[property] &&
typeof themeKey === 'string' &&
typeof property === 'string'
) {
return getMemoizedMapHashKey(
dimensions,
String(themeKey),
String(property),
String(props[property]),
);
} else {
return null;
}
})();

if (memoizedMapHashKey != null) {
const memoizedValue = unsafeTheme.unsafeMemoizedMap[memoizedMapHashKey];
if (memoizedValue != null) {
return memoizedValue;
}
}

const value = (() => {
if (isResponsiveTheme(theme) && dimensions) {
return getResponsiveValue(props[property], {
theme,
dimensions,
themeKey,
transform,
});
} else {
return getThemeValue(props[property], {theme, themeKey, transform});
}
})();
if (value === undefined) return {};

if (memoizedMapHashKey != null) {
unsafeTheme.unsafeMemoizedMap[memoizedMapHashKey] = {
[styleProp]: value,
};
return unsafeTheme.unsafeMemoizedMap[memoizedMapHashKey];
}
return {
[styleProp]: value,
} as {[key in S | P]?: typeof value};
Expand All @@ -45,4 +111,13 @@ const createRestyleFunction = <
};
};

function isResponsiveTheme(
theme: BaseTheme | ResponsiveBaseTheme,
): theme is ResponsiveBaseTheme {
if (theme.breakpoints !== undefined) {
return true;
}
return false;
}

export default createRestyleFunction;
12 changes: 6 additions & 6 deletions src/hooks/useResponsiveProp.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {useWindowDimensions} from 'react-native';

import {BaseTheme, PropValue, ResponsiveValue} from '../types';
import {
getValueForScreenSize,
isResponsiveObjectValue,
} from '../responsiveHelpers';
import {PropValue, ResponsiveBaseTheme, ResponsiveValue} from '../types';
import {getValueForScreenSize, isResponsiveObjectValue} from '../utilities';

import useTheme from './useTheme';

const useResponsiveProp = <Theme extends BaseTheme, TVal extends PropValue>(
const useResponsiveProp = <
Theme extends ResponsiveBaseTheme,
TVal extends PropValue,
>(
propValue: ResponsiveValue<TVal, Theme['breakpoints']>,
) => {
const theme = useTheme<Theme>();
Expand Down
69 changes: 32 additions & 37 deletions src/hooks/useRestyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {useMemo} from 'react';
import {StyleProp, useWindowDimensions} from 'react-native';

import {BaseTheme, RNStyle, Dimensions} from '../types';
import {getKeys} from '../typeHelpers';

import useTheme from './useTheme';

Expand All @@ -13,33 +12,23 @@ const filterRestyleProps = <
componentProps: TProps,
omitPropertiesMap: {[key in keyof TProps]: boolean},
) => {
const props = omitPropertiesMap.variant
? {variant: 'defaults', ...componentProps}
: componentProps;
return getKeys(props).reduce(
({cleanProps, restyleProps, serializedRestyleProps}, key) => {
if (omitPropertiesMap[key as keyof TProps]) {
return {
cleanProps,
restyleProps: {...restyleProps, [key]: props[key]},
serializedRestyleProps: `${serializedRestyleProps}${String(key)}:${
props[key]
};`,
};
} else {
return {
cleanProps: {...cleanProps, [key]: props[key]},
restyleProps,
serializedRestyleProps,
};
}
},
{cleanProps: {}, restyleProps: {}, serializedRestyleProps: ''} as {
cleanProps: TProps;
restyleProps: TRestyleProps;
serializedRestyleProps: string;
},
);
const cleanProps: TProps = {} as TProps;
const restyleProps: TProps & {variant?: unknown} = {} as TProps;
let serializedRestyleProps = '';
if (omitPropertiesMap.variant) {
restyleProps.variant = componentProps.variant ?? 'defaults';
}
for (const key in componentProps) {
if (omitPropertiesMap[key as keyof TProps]) {
restyleProps[key] = componentProps[key];
serializedRestyleProps += `${String(key)}:${componentProps[key]};`;
} else {
cleanProps[key] = componentProps[key];
}
}

const keys = {cleanProps, restyleProps, serializedRestyleProps};
return keys;
};

const useRestyle = <
Expand All @@ -55,7 +44,7 @@ const useRestyle = <
dimensions,
}: {
theme: Theme;
dimensions: Dimensions;
dimensions: Dimensions | null;
},
) => RNStyle;
properties: (keyof TProps)[];
Expand All @@ -64,22 +53,30 @@ const useRestyle = <
props: TProps,
) => {
const theme = useTheme<Theme>();
const dimensions = useWindowDimensions();

// Theme should not change between renders, so we can disable rules-of-hooks
// We want to avoid calling useWindowDimensions if breakpoints are not defined
// as this hook is called extremely often and incurs some performance hit.
const dimensions = theme.breakpoints
? // eslint-disable-next-line react-hooks/rules-of-hooks
useWindowDimensions()
: null;

const {cleanProps, restyleProps, serializedRestyleProps} = filterRestyleProps(
props,
composedRestyleFunction.propertiesMap,
);

const calculatedStyle = useMemo(() => {
const calculatedStyle: StyleProp<RNStyle> = useMemo(() => {
const style = composedRestyleFunction.buildStyle(restyleProps as TProps, {
theme,
dimensions,
});

const styleProp = props.style;
const styleProp: StyleProp<RNStyle> = props.style;
if (typeof styleProp === 'function') {
return (...args: any[]) => [style, styleProp(...args)].filter(Boolean);
return ((...args: any[]) =>
[style, styleProp(...args)].filter(Boolean)) as StyleProp<RNStyle>;
}
return [style, styleProp].filter(Boolean);

Expand All @@ -95,10 +92,8 @@ const useRestyle = <
composedRestyleFunction,
]);

return {
...cleanProps,
style: calculatedStyle,
};
cleanProps.style = calculatedStyle;
return cleanProps;
};

export default useRestyle;
Loading

0 comments on commit a1f37af

Please sign in to comment.