Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

ここから本題 7

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

扱うデータ 人の情報 氏名、メールアドレス、生年月日など グループの情報 名称 グループの所属メンバーリスト グループの階層構造 メタデータ(データ構築に使う) 4000名 x 40属性が入ってくると約16万件のデータを扱う必要が出てくる ※ ここに時系列分の断面が増えるので実際はもっと増える 12

Slide 13

Slide 13 text

ここで出てくる素朴な疑問 Q:一気に全てのデータを取得するから重いのでは? A:その通り Q:じゃあ画面ごとに都度必要なデータを取得すれば良いのでは? A:アーキテクチャの変更からやるととっても大変  (フロントエンドにかなりビジネスロジックが詰まっている) 13

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

1. Chrome DevToolsを用いた計測 16

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

2. 計測結果と考察 24

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

改善方針 本プロジェクトの大部分のデータはImmutableなObjectとして設計されていた  → リアクティブにする必要がない defineReactive が呼ばないようにして、まるっと処理時間を削りたい 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

なぜこれでリアクティブにならなくなるか 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

Slide 35

Slide 35 text

なぜこれでリアクティブにならなくなるか 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

Slide 36

Slide 36 text

なぜこれでリアクティブにならなくなるか あるObjectを Object.freeze すると 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Object.freeze してリアクティブにされないようにする before moutedでstoreから全組織のデータを取得している data: () { return { organizations: [], } }, mounted: () { // store にある組織を全部取得してSet している this.organizations = store.getOrganizations(); } 46

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Object.freezeした後の計測結果 defineReactive の処理部分がまるっと消えた 48

Slide 49

Slide 49 text

もっと速くしたい 49

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

計測結果 52

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

vue-observe-visibility https://github.com/Akryum/vue-observe-visibility Vueで Intersection Observer API を扱うためのライブラリがあるので、今回はこれを使 う 56

Slide 57

Slide 57 text

vue-observe-visibilityの使い方
この要素がViewPort内に入ってきたら visibilityChanged が呼び出される data: () { return { isVisble: false } }, methods: { visibilityChanged(isVisible: boolean): void { this.isVisble = isVisible } } 57

Slide 58

Slide 58 text

今回の組織図での実装方針 58

Slide 59

Slide 59 text

計測結果 59

Slide 60

Slide 60 text

以上の結果 before 60

Slide 61

Slide 61 text

after 61

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

64

Slide 65

Slide 65 text

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