$30 off During Our Annual Pro Sale. View Details »

十数万レコードに耐えうるVue.jsプロジェクトを実現するためのパフォーマンスチューニング

tbashiyy
October 17, 2022
470

 十数万レコードに耐えうるVue.jsプロジェクトを実現するためのパフォーマンスチューニング

Vue Fes Japan Online 2022 での登壇資料です。
https://vuefes.jp/2022/sessions/tbashiyy

tbashiyy

October 17, 2022
Tweet

Transcript

  1. 十数万レコードに耐えうるVue.jsプロジェクトを実現する ためのパフォーマンスチューニング 2022-10-16 Vue Fes Japan 2022 @tbashiyy 1

  2. 自己紹介 柳橋 知弥 / @tbashiyy 株式会社イエソド エンジニア 初登壇がカンファレンスでとても緊張しています 2

  3. 今日のお話 弊社イエソドで、実際に行ったフロントエンドパフォーマンスチューニングのお話 3

  4. YESODとは 企業の人・組織の情報を時系列で一元管理できるマスタ製品 4

  5. YESODとは 1日単位で情報を保持 過去から未来のあらゆる断面における人と組織情報を格納することができる 5

  6. YESODの技術スタック Frontend Vue.js / TypeScript / Vuex / vue-property-decorator (現在2.7でCompositionAPIへ絶賛移行中)

    Backend Kotlin / Ktor / Exposed / Node.js / Express Infrastructure Kubernetes(GKE) / PostgreSQL(CloudSQL) 6
  7. ここから本題 7

  8. ある日 役員「4000人規模の人事データを入れることになったよ 大丈夫?」 エンジニア「確認します!(たぶん大丈夫じゃない)」 8

  9. ある日 エンジニア「(4000人のデータ x 40属性分入れたらめちゃくちゃ重い・・・)」 エンジニア「めちゃくちゃ重くて、修正しないとまずいです!」 9

  10. 当時の状況 ログインしてから初回表示まで十数秒かかる 基本何をしても重い 10

  11. なぜこんなに重くなってしまったのか フロントエンドのアーキテクチャが少し特殊 初回アクセスでほぼ全てのデータを取得してVuexに格納する 格納するデータはImmutableで、更新するときは全部置き換える設計 初回アクセス以降は基本的に更新をしない限りはBackendと通信しない 11

  12. 扱うデータ 人の情報 氏名、メールアドレス、生年月日など グループの情報 名称 グループの所属メンバーリスト グループの階層構造 メタデータ(データ構築に使う) 4000名 x

    40属性が入ってくると約16万件のデータを扱う必要が出てくる ※ ここに時系列分の断面が増えるので実際はもっと増える 12
  13. ここで出てくる素朴な疑問 Q:一気に全てのデータを取得するから重いのでは? A:その通り Q:じゃあ画面ごとに都度必要なデータを取得すれば良いのでは? A:アーキテクチャの変更からやるととっても大変  (フロントエンドにかなりビジネスロジックが詰まっている) 13

  14. じゃあどうするのか 既存のアーキテクチャを変更せずに、頑張って速くする 今日はチーム総出で取り組んだ事例をお話しします 14

  15. 今日の内容 1. Chrome DevToolsを用いた計測 2. 計測結果と考察 3. 事象に対応していく 15

  16. 1. Chrome DevToolsを用いた計測 16

  17. 何よりもまずは計測から 動作検証で発覚した遅い箇所をChrome DevToolsで計測してボトルネックを特定する 17

  18. 計測箇所 1. ログイン後ページが開くまでの処理(データの初期ロード部) 2. 組織図へ遷移する処理(大量要素が表示されるページ) 計測方法 1. パフォーマンスパネルによる計測 2. メモリパネルによる使用量の確認

    18
  19. 計測1. パフォーマンスパネルによる計測 パフォーマンスタブを開き、対象の操作を記録していきます 19

  20. パフォーマンスパネルからわかること 何のMethodがどのくらい処理に時間がかかっているか 20

  21. パフォーマンスパネルからわかること 各Methodの処理時間 21

  22. 計測2. メモリパネルによる計測 利用されているメモリ量を確認することができます 22

  23. Chrome Devtoolsには他にもさまざまな機能があります。 是非一度公式のOverviewに目を通してみてください。 https://developer.chrome.com/docs/devtools/overview/ 23

  24. 2. 計測結果と考察 24

  25. 1. JavaScriptのメモリが1000MB以上利用されていた 2. 大きなデータをリアクティブにするのに数秒かかっていた 3. 大量要素が表示されるページで、 全ての要素のレンダリングが終わってから遷移していて遅くなっていた 25

  26. 2-1. JavaScriptのメモリが1000MB以上利用されていた とにかくメモリが大量に利用されていた。 何かおかしいが、スナップショットを取ろうとするとChromeがクラッシュして取得でき ず・・・ 26

  27. 2-2. 大きなデータをリアクティブにするのに数秒かかっ ていた 4000名x40属性のデータを入れた環境では、初期化に1.5~2秒程度かかっていた 27

  28. 2-3. 要素が大量に描画されるページの初期化が時間がか かっていた 組織図を描画するページで、一つの組織は ~10ms程度だが、500組織近くになるとトータル 5秒ほどかかっていた 28

  29. 3. 事象に対応していく 29

  30. 3-1. JavaScriptのメモリが1000MB以上利用されていた スナップショットもうまく取れず、原因もよくわからなかったのでパス 30

  31. 3-2. 大きなデータをリアクティブにするのに数秒かかっ ていた defineReactive という処理が原因 オブジェクトをリアクティブにするための処理 export function defineReactive( obj:

    object, key: string, val?: any, customSetter?: Function | null, shallow?: boolean, mock?: boolean ) https://github.com/vuejs/vue/blob/ee57d9fd1d51abe245c6c37e6f8f2d45977b929e/src/ core/observer/index.ts#L131 31
  32. 改善方針 本プロジェクトの大部分のデータはImmutableなObjectとして設計されていた  → リアクティブにする必要がない defineReactive が呼ばないようにして、まるっと処理時間を削りたい 32

  33. Vue 2の公式ドキュメント 対象のオブジェクトを Object.freeze(targetObject) する https://jp.vuejs.org/v2/guide/instance.html#データとメソッド 33

  34. なぜこれでリアクティブにならなくなるか defineReactice を呼び出すObserverのコンストラクタ export class Observer { dep: Dep vmCount:

    number // number of vms that have this object as root $data constructor(public value: any, public shallow = false, public mock = false) { // 省略 if (isArray(value)) { // 省略 } else { // 省略 const keys = Object.keys(value) for (let i = 0; i < keys.length; i++) { const key = keys[i] defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock) // defineReactive を呼び出している } } } } https://github.com/vuejs/vue/blob/ee57d9fd1d51abe245c6c37e6f8f2d45977b929e/src/ core/observer/index.ts#L83 34
  35. なぜこれでリアクティブにならなくなるか Observerを生成するmethod export function observe(value: any, shallow?: boolean,  ssrMockReactivity?: boolean):

    Observer | void { // 省略 if { // 省略 } else if ( shouldObserve && (ssrMockReactivity || !isServerRendering()) && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && // <- ここで Object.isExtensible で判定している !value.__v_skip /* ReactiveFlags.SKIP */ ) { ob = new Observer(value, shallow, ssrMockReactivity) } return ob } https://github.com/vuejs/vue/blob/ee57d9fd1d51abe245c6c37e6f8f2d45977b929e/src/ core/observer/index.ts#L120 35
  36. なぜこれでリアクティブにならなくなるか あるObjectを Object.freeze すると 36

  37. 改善結果 まるっと defineReactive の処理時間が消えた! 37

  38. ここでメモリパネルをみてみると・・・ 38

  39. 使用メモリが1/10程度まで、問題1も副次的に解決した! 39

  40. 使用メモリが激減した結果 全体的に何をするにも遅かったが、速くなった さすがに、ブラウザがメモリを数GBも利用すると何をやっても遅くなるっぽい (※ ここは定量的に調査できていません、感覚値です) 40

  41. 3.3 要素が大量に描画されるページの高速化 41

  42. 組織図の表示と操作がめちゃくちゃ重くなっていた 500個の組織図ってどんなものか 42

  43. 500個の組織図ってどんなものか 43

  44. 500個の組織図ってどんなものか もはや見えない 44

  45. 計測結果をもっと見る ここでも defineReactive が呼び出されていて時間がかかってしまっている  → Object.freeze でリアクティブにされないようにする (先ほどと同じ手法) 45

  46. Object.freeze してリアクティブにされないようにする before moutedでstoreから全組織のデータを取得している data: () { return { organizations:

    [], } }, mounted: () { // store にある組織を全部取得してSet している this.organizations = store.getOrganizations(); } 46
  47. after moutedでstoreから全組織のデータを取得しているのは変わらず dataにsetする時に Object.freeze する data: () { return {

    organizations: [], } }, mounted: () { this.organizations = Object.freeze(store.getOrganizations()); } 47
  48. Object.freezeした後の計測結果 defineReactive の処理部分がまるっと消えた 48

  49. もっと速くしたい 49

  50. 全部のコンポーネントがmountされる前に描画を開始する 500近くある組織のうち、画面に表示できるのはせいぜい10組織程度  →全部のコンポーネントのmountedが終わる前に描画を開始してしまう 50

  51. after data: () { return { organizations: [], } },

    mounted: () { const organizations = store.getOrganizations() for (const organization of organizations) { // 一気に全部セットするのではなく、1 個ずつセットしていく this.organizations = Object.freeze([...this.organizations, organization]); await new Promise((resolve) => setTimeout(resolve, 0)); // セットが終わったら描画を走らせるため } } 51
  52. 計測結果 52

  53. さらにここから、追加でチューニング 53

  54. 画面範囲外の要素はレンダリングしないようにする 残りの400近くの組織は画面に表示されないにも関わらず、レンダリングまで実行されてし まう 画面範囲に入ってきたものだけレンダリングするようにする  → Intersection Observer API を利用する 54

  55. Intersection Observer API とは 交差オブザーバー API (Intersection Observer API) は、ターゲットとなる要素が、祖先

    要素または文書の最上位のビューポートと交差する変化を非同期的に監視する方法を提 供します。 交差オブザーバー API では、監視したい要素が他の要素(またはビューポート)に入っ たり出たりしたとき、あるいは両者が交差する量が要求された量だけ変化したときに実 行されるコールバック関数をコードに登録することができます。 https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API これを利用して、組織図の各組織が画面内に入ってきた時のみ、中身のメンバーのコンポー ネントを描画するように制御する 55
  56. vue-observe-visibility https://github.com/Akryum/vue-observe-visibility Vueで Intersection Observer API を扱うためのライブラリがあるので、今回はこれを使 う 56

  57. vue-observe-visibilityの使い方 <div v-observe-visibility="visibilityChanged"> <child-component v-if="isVisible"> </div> この要素がViewPort内に入ってきたら visibilityChanged が呼び出される data:

    () { return { isVisble: false } }, methods: { visibilityChanged(isVisible: boolean): void { this.isVisble = isVisible } } 57
  58. 今回の組織図での実装方針 58

  59. 計測結果 59

  60. 以上の結果 before 60

  61. after 61

  62. まとめ ちょっと特殊な状況におけるVueのパフォーマンスチューニングのお話をしました リアクティブにする必要がない箇所は、特に大きなデータを扱う場合は処理を飛ばすこ とでパフォーマンス改善が見込める 大きなデータをリアクティブにするとかなりメモリも食う 描画する必要のない箇所は必要になってから描画するようにするとより速くなる いずれにせよ、まずは計測が大切 62

  63. まとめ(裏) (ほとんどのケースで可能だと思うが)画面に必要なデータを都度取得するのが一番楽 だと思う Vue 3に上げたら速くなってないかなぁ(移行頑張らなきゃ) 63

  64. 64

  65. ご清聴ありがとうございました 65