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

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

tbashiyy
October 17, 2022
1.1k

 十数万レコードに耐えうる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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  7. ここから本題
    7

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  12. 扱うデータ
    人の情報
    氏名、メールアドレス、生年月日など
    グループの情報
    名称
    グループの所属メンバーリスト
    グループの階層構造
    メタデータ(データ構築に使う)
    4000名 x 40属性が入ってくると約16万件のデータを扱う必要が出てくる

    ここに時系列分の断面が増えるので実際はもっと増える
    12

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    return {

    organizations: [],

    }

    },

    mounted: () {

    // store
    にある組織を全部取得してSet
    している

    this.organizations = store.getOrganizations();

    }

    46

    View Slide

  47. after
    moutedでstoreから全組織のデータを取得しているのは変わらず
    dataにsetする時に Object.freeze
    する
    data: () {

    return {

    organizations: [],

    }

    },

    mounted: () {

    this.organizations = Object.freeze(store.getOrganizations());

    }

    47

    View Slide

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

    View Slide

  49. もっと速くしたい
    49

    View Slide

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

    View Slide

  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

    View Slide

  52. 計測結果
    52

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    56

    View Slide

  57. vue-observe-visibilityの使い方






    この要素がViewPort内に入ってきたら visibilityChanged
    が呼び出される
    data: () {

    return {

    isVisble: false

    }

    },

    methods: {

    visibilityChanged(isVisible: boolean): void {

    this.isVisble = isVisible

    }

    }

    57

    View Slide

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

    View Slide

  59. 計測結果
    59

    View Slide

  60. 以上の結果
    before
    60

    View Slide

  61. after
    61

    View Slide

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

    View Slide

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

    View Slide

  64. 64

    View Slide

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

    View Slide