Slide 1

Slide 1 text

React Native における パフォーマンスチューニング React Native Tech Blog #3 Hisayuki Matsuki Twitter: @mtskhs

Slide 2

Slide 2 text

about me ● 松木久幸(@mtskhs) ○ Backend Engineer / Gopher ○ スポーツAI解説アプリ SpoLive ○ GoにおけるAPI Client @Go Conference Autumn https://speakerdeck.com/matsu0228/api-client-implementation-pattern-in-go ○ fsrpl :Firestore運用ツール (homebrew) https://qiita.com/mtskhs/items/65e66caa6dc9cd706c76 ○ Agile (シリコンバレーで3ヶ月働いて体感した自立型組織のススメ) https://speakerdeck.com/matsu0228/organizationtheory-of-engineering 2

Slide 3

Slide 3 text

Performance Tuning on RN ● こんなことありませんか?? 3

Slide 4

Slide 4 text

Performance Tuning on RN ● こんなことありませんか?? 4 検索結果の表示が遅い

Slide 5

Slide 5 text

Performance Tuning on RN ● こんなことありませんか?? 5 検索結果の表示が遅い データ保存ボタンの反映が遅い

Slide 6

Slide 6 text

Performance Tuning on RN ● こんなことありませんか?? 6 検索結果の表示が遅い ホーム画面の描画が遅い データ保存ボタンの反映が遅い

Slide 7

Slide 7 text

Performance Tuning on RN ● こんなことありませんか?? 7 検索結果の表示が遅い ホーム画面の描画が遅い データ保存ボタンの反映が遅い

Slide 8

Slide 8 text

Assumption 本発表は、下記の前提で進めます ● アーキテクチャ ○ React Native ○ Firebase・Cloud Functions ● プロダクトの初期段階 ○ バックエンドエンジニア不在 / サーバー側処理はできる だけFirebaseで! 8

Slide 9

Slide 9 text

Planing 9 2. クライアント側の高速化 3. サーバー側の高速化 4. レンダリングの最適化 1. 計測・切り分け

Slide 10

Slide 10 text

1-1. Identify bottleneck ● 原因となりうるコードを消してみる ○ 処理が複雑そう(loopが入れ子になっている) ○ firebaseからのデータ取得を複数回している => 2.ClientSide ○ レスポンスが遅そう(CallableFunctions・API通信) => 3.ServerSide ○ レンダリング 回数が多くなりそう・変更しうる変数が多い => 4.Rendering ● 別の画面でも上記を試してみる ○ よくあるのは、別タブの挙動が悪影響していた等 10

Slide 11

Slide 11 text

1-1. Identify bottleneck ● 原因となりうるコードを消してみる 11 const SearchScreen: React.FC = ({ categoryId, publishedDate }) => { const [itemList, setItemList] = useState([]); useEffect(() => { _searchItems(); }, []); const _searchItems = async () => { const items = await tooLateResponseFunc() setItemList(items); }; return ; }; これが遅そうなら?

Slide 12

Slide 12 text

1-1. Identify bottleneck ● 原因となりうるコードを消してみる 12 const SearchScreen: React.FC = ({ categoryId, publishedDate }) => { const [itemList, setItemList] = useState([]); useEffect(() => { _searchItems(); }, []); const _searchItems = async () => { await tooLateResponseFunc() setItemList([{ Id: “**”, name: ”***”, ... }, ….]); }; return ; }; 処理が終わったあとにダミーデータ をset => これで速くなればレンダリングや アニメーションの問題

Slide 13

Slide 13 text

1-1. Identify bottleneck ● 原因となりうるコードを消してみる 13 const SearchScreen: React.FC = ({ categoryId, publishedDate }) => { const [itemList, setItemList] = useState([]); useEffect(() => { _searchItems(); }, []); const _searchItems = async () => { const items = [{ Id: “**”, name: ”***”, ... }, ….] setItemList(items); }; return ; }; 処理を行わずにダミーデータをset => これで速くなれば当該の関数の 問題

Slide 14

Slide 14 text

1-2. measure time ● 処理にかかる時間を計測してみる 14 const measure = async (heavyFunc: () => void, logPrefix?: string) => { const start = new Date().getTime(); await heavyFunc(); const end = new Date().getTime(); console.log(logPrefix || "time: ", end - start, " [ms]"); }; ... cont someFunc= async() => { // something } measure(someFunc, "first")

Slide 15

Slide 15 text

2. Speed up on ClientSide ● 処理をシンプルにする(ループ・ソートのコスト削減) ● Firebaseで 複雑な条件のクエリを/ 複数回発行している => Algolia 検索 => ローカルキャッシュする => サーバで処理して、キャッシュする 15

Slide 16

Slide 16 text

2-1. Simplify code 処理をシンプルにする(ループ・ソートのコスト削減) 16 const getItemsByIds = async (itemIds: string[]) => { const items: any[] = []; for (const id of itemIds) { const item = await getDoc("/items", id); items.push(item); } return items; }; // 100件のテストデータの場合、 29000 [ms]

Slide 17

Slide 17 text

2-1. Simplify code 処理をシンプルにする(ループ・ソートのコスト削減) 17 const getItemList = async () => { const list = await firebase.firestore().collection("/items").get() .then(qs => qs.docs.map(doc => (!doc?.exists ? null : { ...doc.data(), id: doc.id })) ); }; // ループが不要であればなくす、通信回数を減らす // 100件のテストデータの場合、1707 [ms]

Slide 18

Slide 18 text

2-1. Simplify code 処理をシンプルにする(ループ・ソートのコスト削減) 18 // 通信コストが分かる関数名にし、チーム内で共通認識をもつ const fetchItemList = async () => { const list = await firebase.firestore().collection("/items").get() .then(qs => qs.docs.map(doc => (!doc?.exists ? null : { ...doc.data(), id: doc.id })) ); };

Slide 19

Slide 19 text

2-2. Algolia search 複雑なfirebaseクエリのかわりにAlgolia を利用する 19 // 特定日以降に公開済で、カテゴリが1 or 2に一致する商品を探す // doc: https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filters-and-facetfilters/#multiple-filters const query= { query: "*", filters: `published=1 AND publishedAtUnix > ${unixTime} AND ( categoryId=${cat1} OR categoryId=${cat2} )`, };

Slide 20

Slide 20 text

2-2. Algolia search 複雑なfirebaseクエリのかわりにAlgolia を利用する 20 // クエリを発行する例 doc: https://www.algolia.com/doc/guides/getting-started/quick-start/tutorials/quick-start-with-the-api-client/javascript/?language=javascript#ini tialize-the-client const index = algoliasearch(***).initIndex(indexName); const resp = await index.search("*", query) if (resp?.hits && resp.hits.length) { hits.map(hit => { console.log(hit) }) }

Slide 21

Slide 21 text

2-3. Local cache リアルタイムに変化せず、複数回/箇所で利用するデータ 21 // cache作成時刻と、どのくらいの期間有効とするかを指定 export type Cache = { data: any; expires: number; createdAt: dayjs.Dayjs; }; type CacheMap = { [key: string]: Cache; }; let cacheList: CacheMap = {};

Slide 22

Slide 22 text

2-3. Local cache リアルタイムに変化せず、複数回/箇所で利用するデータ 22 export const saveToCache = ({ key, data, expires = null}: CacheParam): CacheError | null => { if (!data) { return "invalid data"; } cacheList[key] = { data, expires: expires || defaultExpires, createdAt: dayjs() }; return null; };

Slide 23

Slide 23 text

2-3. Local cache リアルタイムに変化せず、複数回/箇所で利用するデータ 23 export const loadFromCache = ({ key }: CacheParam): Cache | null => { if (!cacheList.hasOwnProperty(key)) { return null; } const cache = cacheList[key]; if (!cache?.createdAt || !cache.data) { return null; } const { expires, createdAt } = cache; if (!createdAt.isAfter(dayjs().subtract(expires, "millisecond"))) { delete cacheList[key]; return null; } return cache; //expireしていないものだけを返す };

Slide 24

Slide 24 text

2-4. Server cache どのユーザーも共通のデータを利用する場合はサーバー側で処 理してcacheさせておく方法も(キャッシュ方法は後述) 24 // local処理を、手軽にサーバー側に移行するには // firebase callable functionを使うと認証が楽でよい // Cloud Function側 export const tooHeavyFunc = async ( postData: PostData, context: functions.https.CallableContext ) => { // something }

Slide 25

Slide 25 text

2-4. Server cache どのユーザーも共通のデータを利用する場合はサーバー側で処 理してcacheさせておく方法も(キャッシュ方法は後述) 25 // アプリ側 const heavyFunc = (params) => { return new Promise(async (resolve, reject) => { setTimeout(() => reject("timeout"), 8000); try { const result = await firebase.app().functions("asia-northeast1") .httpsCallable("tooHeavyFunc")(params); resolve(result); } catch (err) { reject(err.details.code); } }); };

Slide 26

Slide 26 text

3. Speed up on ServerSide ● CQRSとサーバ側キャッシュ ● firebase 側に事前に非正規データを用意しておく ● Cloud Functionsの起動を速くする 26

Slide 27

Slide 27 text

3-1. CQRS & Server cache ● どのユーザーも共通のデータを利用する場合で読み込みが遅い 場合は、サーバー側で処理してcacheさせておくとよい ● CQRS: コマンドクエリ責務分離。書き込みと読み込みのモデル を分け、読み込みのパフォーマンス向上などを狙う 27 // 書き込みデータ /itemData / category / {categoryId} / items / {itemId} => カテゴリごとに、異なるスキーマを管理しやすい・権限割当がしやすい一 方で、複数カテゴリを読み込みためにはコストがかかる // 読み込みデータ /itemForSeach / {itemId} ※category情報はitem内のフィールドに持たせる => クエリ1回で複数カテゴリを読み込める。サーバー側で(1)書き込み用デー タから特定の集計を行い、読み込みデータを作成しておく (2)レスポンスを返 すとすると、2回目以降はレスポンスが早くなる

Slide 28

Slide 28 text

3-2. Firestore triggers for cache 前述のCQRSパターンを用いる場合、firebase hooksを利用して 事前にデータを用意することも可能 28 // cloud functions modules.exports.firestoreOnwriteItems = functions .region("asia-northeast1") .firestore.document( "/itemData/category/{categoryId}/items/{itemId}" ) .onWrite(onWriteItems); パスを定義しておくと、データ 変更を検知して onWriteItems を実行することができる

Slide 29

Slide 29 text

3-2. Firestore triggers for cache 前述のCQRSパターンを用いる場合、firebase hooksを利用して 事前にデータを用意することも可能 29 // cloud functions export const onWriteItems = async ( snapshot: any, context: functions.EventContext ): Promise => { const { categoryId, itemId } = context.params; const before = snapshot.before.data(); const after = snapshot.after.data(); // do something } パスに含まれる変数を使える 変更前・変更後のデータを参照 できる ここで読み込み用データを生成・ 保存することが可能

Slide 30

Slide 30 text

3-3. Quick start of CloudFunctions CloudFunctionsは肥大化していくと、起動時の処理が遅くなる そこで、関数名で読み込むファイルに制限をかけるとよい 30 // functionsNameで読み込むファイルの制限をかける // Node10 では、サービス名を参照する環境変数名が変わっている if (!process.env.K_SERVICE || process.env.K_SERVICE.startsWith("firestore")) { const Firestore = require("./firestore"); module.exports.firestore = { ...Firestore }; }

Slide 31

Slide 31 text

4. Optimisation of Rendering ● hooks化 ● ページングする 31

Slide 32

Slide 32 text

4-1. Class to Hooks 基本的にHooks化しておいたほうがデータ変更時にレンダリングが 走る箇所が限定的になるのでパフォーマンスがよい 32 // componentDidMount(){} // componentWillUnMount(){} をhooksで書くと useEffect(() => { const subscription = props.source.subscribe(); console.log(“mount”) return () => { console.log(“unMount”) subscription.unsubscribe(); }; });

Slide 33

Slide 33 text

4-1. Class to Hooks 基本的にHooks化しておいたほうがデータ変更時にレンダリングが 走る箇所が限定的になるのでパフォーマンスがよい 33 // componentWillUpdate(){} をhooksで書くと // 直前の値を取得する関数は、 useRef()を使って自前で用意するのが 慣例のよう const usePrevious = (v) => { const ref = useRef(); useEffect(() => { ref.current = v; }, [v]); return ref.current; };

Slide 34

Slide 34 text

4-1. Class to Hooks 基本的にHooks化しておいたほうがデータ変更時にレンダリングが 走る箇所が限定的になるのでパフォーマンスがよい 34 // componentWillUpdate(){} をhooksで書くと const [isLoading, setIsLoading] = useState(false); const prevIsLoading: boolean = usePrevious(isLoading); useEffect(() => { if (!prevIsLoading && isLoading) { // do something } }, [isLoading]); 特定の値の変更があった場合にだけ、 処理が実行される

Slide 35

Slide 35 text

4-2. limit data FlatListなどのrenderingはデータが多いほどパフォーマンスが落ち やすいので、ページングすると初回描画の速度が向上する 35 const ONCE_DISPLAY_LIMIT = 10 const [currentIndex, setCurrentIndex] = useState(0); const onMoreItems =() => { const endIndex = currentIndex + ONCE_DISPLAY_LIMIT; const renderItemList = itemList.slice(currentIndex, endIndex); setCurrentIndex(endIndex) } // ... return ( { if (isCloseToBottom(nativeEvent) && !allLoaded) { onMoreItems(); } }} )

Slide 36

Slide 36 text

Summary プロダクト初期段階における ReactNativeのパフォーマンスチューニングのお話 ● まずは 当たりをつける ● それぞれの具体的なTipsを紹介 ○ クライアント側の高速化 ○ サーバー側の高速化 ○ レンダリングの最適化 ● 他にもあれば/もっとよい方法があれば 教えて下さい 36

Slide 37

Slide 37 text

SpoLive ラグビー・サッカーの試合に対応中 37