Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve useRestyle performance #131

Merged
merged 1 commit into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions src/composeRestyleFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BaseTheme,
Dimensions,
RNStyle,
RestyleFunction,
} from './types';
import {AllProps} from './restyleFunctions';

Expand All @@ -26,18 +27,19 @@ const composeRestyleFunctions = <
const properties = flattenedRestyleFunctions.map(styleFunc => {
return styleFunc.property;
});
const funcs = flattenedRestyleFunctions
.sort(
(styleFuncA, styleFuncB) =>
Number(styleFuncB.variant) - Number(styleFuncA.variant),
)
.map(styleFunc => {
return styleFunc.func;
});
const propertiesMap = properties.reduce(
(acc, prop) => ({...acc, [prop]: true}),
{} as Record<keyof TProps, true>,
);

const funcsMap = flattenedRestyleFunctions.reduce(
(acc, each) => ({[each.property]: each.func, ...acc}),
{} as Record<keyof TProps, RestyleFunction<TProps, Theme, string>>,
);

// TInputProps is a superset of TProps since TProps are only the Restyle Props
const buildStyle = <TInputProps extends TProps>(
props: TInputProps,
const buildStyle = (
props: TProps,
{
theme,
dimensions,
Expand All @@ -46,15 +48,20 @@ const composeRestyleFunctions = <
dimensions: Dimensions;
},
): RNStyle => {
const styles = funcs.reduce((acc, func) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each style function in funcs takes all the props of the component, applies a transformation and returns a style attribute that then will be used to create a stylesheet. If there is no value to transform, each function returns an empty object instead.

Here, we were running every style function every time even though, not all transformations were needed. Example:

<Box marginTop="m" />

Here we only need to transform marginTop="m" to marginTop: 16, however we run all the transformations registered for the Box component here: https://github.com/sbalay/restyle-benchmark/blob/master/src/restyle/createBox.ts#L47

So we also check for backgroundColor and return an empty object, we also check for marginBottom and returns an empty object, we also check for paddingHorizontal and so on.

With the change below, we only run transformation functions for the props that are present. In the example above we would only run the function that transforms marginTop="m" to {marginTop: 16}

return Object.assign(acc, func(props, {theme, dimensions}));
}, {});
const styles = Object.keys(props).reduce(
(styleObj, propKey) => ({
...styleObj,
...funcsMap[propKey as keyof TProps](props, {theme, dimensions}),
}),
{},
);
const {stylesheet} = StyleSheet.create({stylesheet: styles});
return stylesheet;
};
return {
buildStyle,
properties,
propertiesMap,
};
};

Expand Down
5 changes: 4 additions & 1 deletion src/createRestyleComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {View} from 'react-native';

import composeRestyleFunctions from './composeRestyleFunctions';
import {BaseTheme, RestyleFunctionContainer} from './types';
import useRestyle from './hooks/useRestyle';

Expand All @@ -13,8 +14,10 @@ const createRestyleComponent = <
| RestyleFunctionContainer<Props, Theme>[])[],
BaseComponent: React.ComponentType<any> = View,
) => {
const composedRestyleFunction = composeRestyleFunctions(restyleFunctions);

const RestyleComponent = React.forwardRef((props: Props, ref) => {
const passedProps = useRestyle(restyleFunctions, props);
const passedProps = useRestyle(composedRestyleFunction, props);
return <BaseComponent ref={ref} {...passedProps} />;
});
type RestyleComponentType = typeof RestyleComponent;
Expand Down
64 changes: 35 additions & 29 deletions src/hooks/useRestyle.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {useMemo} from 'react';
import {StyleProp} from 'react-native';
import {StyleProp, ViewStyle, TextStyle, ImageStyle} from 'react-native';

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

import useDimensions from './useDimensions';
Expand All @@ -13,24 +12,20 @@ const filterRestyleProps = <
TProps extends Record<string, unknown> & TRestyleProps
>(
props: TProps,
omitList: (keyof TRestyleProps)[],
): Omit<TProps, keyof TRestyleProps> => {
const omittedProp = omitList.reduce<Record<keyof TRestyleProps, boolean>>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every time we were going to filter the props to distinguish between restyle props and non restyle props, we would build this map.

Now, we build the map only once when calling createRestyledComponent and use that here.

(acc, prop) => {
acc[prop] = true;
return acc;
},
{} as Record<keyof TRestyleProps, boolean>,
);

omitPropertiesMap: Record<keyof TProps, boolean>,
) => {
return getKeys(props).reduce(
(acc, key) => {
if (!omittedProp[key as keyof TRestyleProps]) {
acc[key] = props[key];
({cleanProps, restyleProps}, key) => {
if (omitPropertiesMap[key as keyof TProps]) {
return {cleanProps, restyleProps: {...restyleProps, [key]: props[key]}};
} else {
return {cleanProps: {...cleanProps, [key]: props[key]}, restyleProps};
}
return acc;
},
{} as TProps,
{cleanProps: {}, restyleProps: {}} as {
cleanProps: TProps;
restyleProps: TRestyleProps;
},
);
};

Expand All @@ -39,28 +34,39 @@ const useRestyle = <
TRestyleProps extends Record<string, any>,
TProps extends TRestyleProps & {style?: StyleProp<RNStyle>}
>(
restyleFunctions: (
| RestyleFunctionContainer<TProps, Theme>
| RestyleFunctionContainer<TProps, Theme>[])[],
composedRestyleFunction: {
buildStyle: <TInputProps extends TProps>(
props: TInputProps,
{
theme,
dimensions,
}: {
theme: Theme;
dimensions: Dimensions;
},
) => ViewStyle | TextStyle | ImageStyle;
properties: (keyof TProps)[];
propertiesMap: Record<keyof TProps, boolean>;
},
props: TProps,
) => {
const theme = useTheme<Theme>();

const dimensions = useDimensions();

const restyled = useMemo(() => {
const composedRestyleFunction = composeRestyleFunctions(restyleFunctions);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restyleFunctions are static values, so we don't need to compose them on every render cycle.

We can do that only once when calling createRestyledComponent instead

const style = composedRestyleFunction.buildStyle(props, {
const {cleanProps, restyleProps} = filterRestyleProps(
props,
composedRestyleFunction.propertiesMap,
);
const style = composedRestyleFunction.buildStyle(restyleProps, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were calling composedRestyleFunction.buildStyle with all the props of the components, but we only need to call it with the props that belong to restyle.

theme,
dimensions,
});
const cleanProps = filterRestyleProps(
props,
composedRestyleFunction.properties,
);
(cleanProps as TProps).style = [style, props.style].filter(Boolean);

cleanProps.style = [style, props.style].filter(Boolean);
return cleanProps;
}, [restyleFunctions, props, dimensions, theme]);
}, [composedRestyleFunction, props, dimensions, theme]);

return restyled;
};
Expand Down
19 changes: 13 additions & 6 deletions src/test/useRestyle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Text, TouchableOpacity} from 'react-native';
import useRestyle from '../hooks/useRestyle';
import {position, PositionProps} from '../restyleFunctions';
import createVariant, {VariantProps} from '../createVariant';
import composeRestyleFunctions from '../composeRestyleFunctions';

const theme = {
colors: {},
Expand All @@ -15,21 +16,27 @@ const theme = {
phone: 0,
tablet: 376,
},
zIndices: {
phone: 5,
},
};
type Theme = typeof theme;

type Props = VariantProps<Theme, 'buttonVariants'> &
PositionProps<Theme> & {
title: string;
} & ComponentPropsWithoutRef<typeof TouchableOpacity>;
PositionProps<Theme> &
ComponentPropsWithoutRef<typeof TouchableOpacity>;

const restyleFunctions = [
position,
createVariant({themeKey: 'buttonVariants'}),
createVariant<Theme>({themeKey: 'buttonVariants'}),
];

function Button({title, ...rest}: Props) {
const props = useRestyle(restyleFunctions, rest);
const composedRestyleFunction = composeRestyleFunctions<Theme, Props>(
restyleFunctions,
);

function Button({title, ...rest}: Props & {title: string}) {
const props = useRestyle(composedRestyleFunction, rest);
return (
<TouchableOpacity {...props}>
<Text>{title}</Text>
Expand Down