We used console to debug a mobile app in development environment. After that, we have to remove for performing the app in production.
- use lint code to detect all statements
- use babel plugins to remove all statements from production such as
babel-plugin-transform-remove-console
ScrollView
is simple to implement. However, it renders all children at once. Therefore, we should use Flatlist
, SectionList
with huge data.
// bad
<ScrollView>
{items.map(item => {
return <Item key={item.id.toString()} name={item.name} />;
})}
</ScrollView>
// good
<FlatList
data={items}
keyExtractor={item => `${items.id}`}
renderItem={({ item }) => <Item name={item.name} />}
/>
Don’t use arrow functions as callbacks in your functions to render views. With use arrow function, each renders generates a new instance of function and finally the child component which used it will be rendered because of detecting new props.
// bad
function Todo() {
function addTodo() {
// ...
}
return (<Pressable onPress={() => addTodo()} />)
}
// good
function Todo() {
function addTodo() {
// ...
}
return (<Pressable onPress={addTodo} />)
}
- debounce press handling
- make a overlay to prevent double press
Heavy component such as huge lists will lead to high memories on JS thread. That's why we should wait for transitions will be complete.
Fortunately, the useIsFocused
hook provides its status.
import { useIsFocused } from '@react-navigation/native';
// ...
function ProfileListScreen() {
const isFocused = useIsFocused();
// return list;
}
-
Firebase
-
Sentry
Returns a memoized value of a function. It should be used only when we want perform expensive computations such as handling huge data, object...
const memoizedResult = useMemo(() => compute(a, b), [a, b]);
when next renderings, the dependencies don't change, then useMemo()
doesn't invoke compute
but returns the memoized value. By using useMemo
, we can memoize the results.
For example,
const data = [
{id: 1, value: 1},
// ...
{id: 10000, value: 10000}
];
const memoizeData = useMemo(() => {
const filterData = data.filter(...);
const mappedData = filterData.map(...);
// ...
const finalValue = mappedData.reduce(...);
return finalValue;
}, [data]);
Returns a memoized callback. Similar to useMemo
, it used but it returns a memoized callback. It only is changed if one of dependencies has changed.
const todoCallback = useCallback(
() => {
doSomething(a, b)
},
[a, b],
);
// ...
return(<Button onPress={todoCallback}/>);
// ...
Return a callback which depends on its parameter.
const onChangeValue = useCallback(
(fieldId) => (newValue) => {
handleChangeValue(fieldId, newValue)
},
[]
);
It is useful when passing callbacks to child component to prevent unnecessary renders.
export default function Maker() {
const handleTakeMaker = useCallback(() => {
// making point
}, []);
return (
<View style={styles.container}>
<MakerItem onPress={handleTakeMaker} />
</View>
);
}
memo
only checks for prop changes. This means that React will skip rendering the component, and reuse the last rendered result.
function Movie({ title }) {
return (
<Text>Movie title: {title}</Text>
);
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export const MemoizedMovie = React.memo(Movie, areEqual);
// By default it will only shallowly compare objects in the props object.
// export const MemoizedMovie = React.memo(Movie);
It is important to optimize images to improve RN app's performance. Its make reduce download time and memory usage on the device. In other way, we should resize the image size before loading them on the app.
Recommend: Cloudinary
Using PNG files as a static image of your react native app will also lead you to the problem of memory leaking. This is because react native use fresco library
to render and display images. This can be solved by using JPG images which will reduce the memory footprint as a result.
Caching is another solution to image problems in a React Native app. It saves the images locally the first time they are loaded and uses the local cache in the subsequent requests.
Highly recommending to use react-native-fast-image for caching images on react native app.
Image.prefetch
or FastImage.preload
for later display by downloading it to the disk cache.
<Flatlist
data={data}
renderItem={renderItem}
onEndReachedThreshold={0.2}
onEndReached={onEndReached}
// ...
refreshing={refreshing}
onRefresh={onRefresh}
/>
onEndReachedThreshold
is used to determine how far the distance from the bottom in order to trigger onEndReached
.
onRefresh
will be called when pulling the list.
To feel UI smoothly, we should create a loader component or a progressive image before load a component completely.
refs: https://skeletonreact.com/
refs: https://reactnative.dev/docs/optimizing-flatlist-configuration
The JavaScript thread is responsible for controlling navigator animations. When rendering a new screen while an animation is running on the JavaScript thread, it results in broken animations.
Therefore, we should use InteractionManager
to avoid broken frames.
InteractionManager.runAfterInteractions(() => {
// ...
});
This would run the animation during the next layout
const AnotherComponent = () => {
// ...
function handleClick() {
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring)
// ...
}
// ...
}
Make sure the flag has to be set for works on Android.
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
Using nativeDriver
to send animations over the native bridge before the animation starts on the screen. it reduces workload on JS thread
and move to UI thread
.
Animated.timing(opacity, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
Other suggest is using Reanimated 2. It provides new approach for making new smooth animations and interactions.
The problem with controlled inputs in React Native is that on slower devices or when a user is typing really fast, rendering may occur while updating the view.
For example,
function UncontrolledInputs() {
const [text, onTextChange] = React.useState('Controlled inputs');
return (
<TextInput
style={styles.input}
onChangeText={onTextChange}
defaultValue={text}
/>
);
}
Consider to you react-hook-form if the screen has more than 2 TextInput.
Hermes is an open-source JavaScript engine optimized for React Native. Enabling Hermes will result in
- improved start-up time
- decreased memory usage
- smaller app size
Edit android/app/build.gradle
file
project.ext.react = [
entryFile: "index.js",
- enableHermes: false // clean and rebuild if changing
+ enableHermes: true // clean and rebuild if changing
]
-keep class com.facebook.hermes.unicode.** { *; }
-keep class com.facebook.jni.** { *; }
Then, clean and build project.
edit ios/Podfile
file
use_react_native!(
:path => config[:reactNativePath],
- :hermes_enabled => flags[:hermes_enabled],
+ :hermes_enabled => true
)
Then install Hermes pods with
$ cd ios && pod install
On iOS, edit ../node_modules/react-native/scripts/react-native-xcode.sh
export BUNDLE_COMMAND="ram-bundle" // add this
export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh
On Android
edit android/app/build.gradle
project.ext.react = [
bundleCommand: "ram-bundle",
]
Note: If you are using the Hermes JS Engine, you do not need RAM bundles
Enabling Proguard to reduce the size of the APK
// android/app/build.gradle
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = true
Set enableSeparateBuildPerCPUArchitecture = true
. Android devices support two major device artitectures armebi and x86. By default RN builds the native librariers for both these artitectures into the same apk.