Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ReactNativeにおけるパフォーマンスチューニング/ Performance tun...

mtskhs
May 27, 2020

ReactNativeにおけるパフォーマンスチューニング/ Performance tuning in ReactNative

[React Native Tech Blog #3](https://ducklings.connpass.com/event/174638/) におけるLT資料です。

もともとGoなどバックエンドが得意な自分が、ReactNativeによるアプリのパフォーマンス向上を行った際の知見を共有しています。アプリ内だけの改善にとどまらず、Firebase・ClouldFunctions含めて、どのようなときにどのような改善ができるかが分かる内容となっています。

mtskhs

May 27, 2020
Tweet

More Decks by mtskhs

Other Decks in Programming

Transcript

  1. 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
  2. Assumption 本発表は、下記の前提で進めます • アーキテクチャ ◦ React Native ◦ Firebase・Cloud Functions

    • プロダクトの初期段階 ◦ バックエンドエンジニア不在 / サーバー側処理はできる だけFirebaseで! 8
  3. 1-1. Identify bottleneck • 原因となりうるコードを消してみる ◦ 処理が複雑そう(loopが入れ子になっている) ◦ firebaseからのデータ取得を複数回している =>

    2.ClientSide ◦ レスポンスが遅そう(CallableFunctions・API通信) => 3.ServerSide ◦ レンダリング 回数が多くなりそう・変更しうる変数が多い => 4.Rendering • 別の画面でも上記を試してみる ◦ よくあるのは、別タブの挙動が悪影響していた等 10
  4. 1-1. Identify bottleneck • 原因となりうるコードを消してみる 11 const SearchScreen: React.FC<Props> =

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

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

    ({ categoryId, publishedDate }) => { const [itemList, setItemList] = useState<Item[]>([]); useEffect(() => { _searchItems(); }, []); const _searchItems = async () => { const items = [{ Id: “**”, name: ”***”, ... }, ….] setItemList(items); }; return <SearchResult itemList={itemList} />; }; 処理を行わずにダミーデータをset => これで速くなれば当該の関数の 問題
  7. 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")
  8. 2. Speed up on ClientSide • 処理をシンプルにする(ループ・ソートのコスト削減) • Firebaseで 複雑な条件のクエリを/

    複数回発行している => Algolia 検索 => ローカルキャッシュする => サーバで処理して、キャッシュする 15
  9. 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]
  10. 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]
  11. 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 })) ); };
  12. 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} )`, };
  13. 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) }) }
  14. 2-3. Local cache リアルタイムに変化せず、複数回/箇所で利用するデータ 21 // cache作成時刻と、どのくらいの期間有効とするかを指定 export type Cache

    = { data: any; expires: number; createdAt: dayjs.Dayjs; }; type CacheMap = { [key: string]: Cache; }; let cacheList: CacheMap = {};
  15. 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; };
  16. 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していないものだけを返す };
  17. 2-4. Server cache どのユーザーも共通のデータを利用する場合はサーバー側で処 理してcacheさせておく方法も(キャッシュ方法は後述) 24 // local処理を、手軽にサーバー側に移行するには // firebase

    callable functionを使うと認証が楽でよい // Cloud Function側 export const tooHeavyFunc = async ( postData: PostData, context: functions.https.CallableContext ) => { // something }
  18. 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); } }); };
  19. 3-1. CQRS & Server cache • どのユーザーも共通のデータを利用する場合で読み込みが遅い 場合は、サーバー側で処理してcacheさせておくとよい • CQRS:

    コマンドクエリ責務分離。書き込みと読み込みのモデル を分け、読み込みのパフォーマンス向上などを狙う 27 // 書き込みデータ /itemData / category / {categoryId} / items / {itemId} => カテゴリごとに、異なるスキーマを管理しやすい・権限割当がしやすい一 方で、複数カテゴリを読み込みためにはコストがかかる // 読み込みデータ /itemForSeach / {itemId} ※category情報はitem内のフィールドに持たせる => クエリ1回で複数カテゴリを読み込める。サーバー側で(1)書き込み用デー タから特定の集計を行い、読み込みデータを作成しておく (2)レスポンスを返 すとすると、2回目以降はレスポンスが早くなる
  20. 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 を実行することができる
  21. 3-2. Firestore triggers for cache 前述のCQRSパターンを用いる場合、firebase hooksを利用して 事前にデータを用意することも可能 29 //

    cloud functions export const onWriteItems = async ( snapshot: any, context: functions.EventContext ): Promise<void> => { const { categoryId, itemId } = context.params; const before = snapshot.before.data(); const after = snapshot.after.data(); // do something } パスに含まれる変数を使える 変更前・変更後のデータを参照 できる ここで読み込み用データを生成・ 保存することが可能
  22. 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 }; }
  23. 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(); }; });
  24. 4-1. Class to Hooks 基本的にHooks化しておいたほうがデータ変更時にレンダリングが 走る箇所が限定的になるのでパフォーマンスがよい 33 // componentWillUpdate(){} をhooksで書くと

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

    const [isLoading, setIsLoading] = useState<boolean>(false); const prevIsLoading: boolean = usePrevious(isLoading); useEffect(() => { if (!prevIsLoading && isLoading) { // do something } }, [isLoading]); 特定の値の変更があった場合にだけ、 処理が実行される
  26. 4-2. limit data FlatListなどのrenderingはデータが多いほどパフォーマンスが落ち やすいので、ページングすると初回描画の速度が向上する 35 const ONCE_DISPLAY_LIMIT = 10

    const [currentIndex, setCurrentIndex] = useState<number>(0); const onMoreItems =() => { const endIndex = currentIndex + ONCE_DISPLAY_LIMIT; const renderItemList = itemList.slice(currentIndex, endIndex); setCurrentIndex(endIndex) } // ... return ( <ScrollView onScroll={({ nativeEvent }) => { if (isCloseToBottom(nativeEvent) && !allLoaded) { onMoreItems(); } }} )