🍀

弊社のフロントエンドのパフォーマンス改善のお話

2024/09/11に公開

はじめに

はじめまして、BEENOSの山岡です。
普段は主にフロントエンドの開発を担当しており、執筆時点では新卒3年目となりました(時が経つのは早いですね…)
今回は、弊社が運用している大規模Webサービスのフロントエンドのパフォーマンス改善について、振り返りも兼ねて共有したいと思います✏️

問題

今回の取り組みの前に抱えていたプロダクトの問題点としては、以下の2点です。
①レンダリング後にAPIをたたいた後、再度データを取得する際に、再び同じAPIをたたいてしまっている。
②APIのレスポンスの値に対して、都度storeの枠を増やす必要があり、それを行わずに別の場所で再ロードをすると、前回のデータが連鎖的に変わってしまう現象が発生する。(cloneDeepなどを使ってオブジェクトをディープコピーすることで対処できますが、cloneDeepによって作成された新しいオブジェクトはリアクティブなトラッキングから外れてしまい、その結果、Vueコンポーネントが状態の変更を検知できなくなります。)

技術構成

  • 使用言語:TypeScript
  • フレームワーク:Vue.js
  • 状態管理:Vuex

Vuexのおさらい

(普段の実装でVuexを使わない方のために)軽くVuexのおさらいをします。
Vuexの場合、リクエストからレスポンスが返されるまでは以下のフローでStateの状態が管理されます。

  • State:アプリケーションの保持したい値
  • Getter:Stateの一部や、stateから計算された値を返す
  • Mutation:Stateの状態を更新
  • Action:非同期処理や、LocalStrageへの読み書きのような外部APIとのやりとりを担う

参照元:Vuex公式ページ「Vuexとは何か?」

開発方針

先述した現在のstoreの問題に対して、以下の開発方針で進めました。

  • store内のデータを配列で管理し、ロード時も取得時も同じキーで結びつける
  • キーをユニークな文字列に変換する関数を用意
  • 取得時にデータの保存時間を記録し、有効期間内の再取得ではAPIを呼び出さないようにする
  • store内の配列には上限を設定し、新しいデータが追加された際には古いデータから順に削除されるようにする
  • データを再読み込みすると、既存・新規に関わらずリストの一番最後に移動する
  • 既存データを再取得する場合、取得時間も更新する

改修前

改修後

storeの中身を見て、取得対象の値が読み込まれているかどうかを確認し、すでに読み込まれている場合はAPIはたたかず、読み込まれていなければAPIをたたくようにする

開発内容

※一部抜粋

  1. Stateの対象の型を修正(StoreItemでラップして配列化)

Before

productName: string; // 商品
productList: HogeItem[]; // 商品一覧 

After

productName: StoreItem<string>[];
productList: StoreItem<HogeItem[]>[];

/**
 * Getアクション共通のストアアイテム用interface
 */
export interface StoreItem<T> {
  storeKey: string;
  data: T
}
  1. Actionからstate(storeItem一覧)を取る用にprivateのgetterを新規作成

Before

なし

After

private get productNameStoreItems(): StoreItems<string>[] {
  return this.st.productName;
}
private get productListStoreItems(): StoreItems<HogeItem[]>[] {
  return this.st.productList;
}
  1. 実際に利用するgetterをActionのstoreKeyと同様のstoreKeyで取り出す形に修正

Before

get productName(): string {
  return this.st.productName;
}
get productList(): HogeItem[] {
  return this.st.productList;
}

After

get productName(): (hogeId: number, storeKeySuffix?: string) => string {
  return (hogeId, storeKeySuffix) => {
    const storeKey = generateStoreKey(hogeId, storeKeySuffix);
    return getCache(this.st.productName, storeKey);
  }
}
get productList(): (listId: number, storeKeySuffix?: string) => HogeItem[] {
  return (listId, storeKeySuffix) => {
    const storeKey = generateStoreKey(listId, storeKeySuffix);
    return getCache(this.st.productList, storeKey);
  }
}
  1. mutationをstoreItem型を保存する形に修正

Before

@Mutation
setProductName(productName: string): void {
  this.st.productName = name;
}
@Mutation
setProductList(productList: HogeItem[]): void {
  this.st.productList = list;
}

After

@Mutation
setProductName(storeItem: StoreItem<string>): void {
  this.st.productName = updatedStoreItemList(this.st.productName, storeItem);
}
@Mutation
setProductList(storeItem: StoreItem<HogeItem[]>): void {
  this.st.productList = updatedStoreItemList(this.st.productList, storeItem);
}
  1. actionの引数に、専用キーを含ませたオブジェクトで渡すように修正し、API呼び出し前の処理を追加する

Before

@Action({ commit: "setProductName", rawError: true })
async loadProductName(hogeId: number): Promise<string> {
  try {
    const response = await hogeApi.get.call(hogeId);
    return response.data.productName;
  }
}

After

@Action({ rawError: true })
async loadProductName(payload: { hogeId: number } & CacheCntrolPayload): Promise<void> {
  const storeCtrl: StoreControl = {
    // storeの中に保存する識別子を作成
    storeKey: generateStoreKey(payload.hogeId, payload.keySuffix)
  };
  // 「savedStoreItemWhenShouldNotBeLoaded」で、再読み込みするべきかどうかを判定
  const savedStoreItem = savedStoreItemWhenShouldNotBeLoaded(this.nameStoreItem, storeCtrl);
  // すでに読み込んでいる場合
  if (savedStoreItem){
    this.setProductName(savedStoreItem);
    return;
  }
  // 初回の読み込みの場合、APIをたたき手順1のstoreItemに保存をする
  try {
    const response = await hogeApi.get.call(payload.hogeId);
    const pruductName = response.data.productName;
    //
    const newStoreItem = storeItemFactory.make({
      storeCtrl,
      data: productName
    });
    this.setProductName(newStoreItem);
  }
}

まとめ

既存のコードのパフォーマンス改善に取り組む際、コードを修正するだけでなく、既存の機能に影響が出ないかどうかを考慮することに苦労しました。
また、今回のVuexの改善を通じて、日常の開発でもパフォーマンスに問題がないかという意識が以前と比べると養われたと感じています。

今回の改善は、まだ適用できていないページがあるほか、Vuex以外のフロントエンドのパフォーマンス改善や最適化についてもまだ途中段階です。
改修内容について不足している要素がある可能性もありますが、その際は別の記事で改めて取り上げたいと思います…🙏

今回の内容が、皆さんの開発の一助となれば幸いです。

Wanted!

BEENOSグループでは一緒に働いて頂けるエンジニアを強く求めております!
少し気になった方は、社内の様子や大事にしていることなどをThe BEENOSにて発信しておりますので、是非ご覧ください。

とても気になった方はこちらでも求人を公開しておりますので、お気軽にご応募ください!
「自分に該当する職種がないな...?」と思った方は オープンポジション としてご応募頂けると大変嬉しく思います🙌
世界で戦えるサービスを創っていきたい方、是非是非ご連絡ください!よろしくお願い致します!!

世界で戦えるサービスを創っていく

BEENOS Tech Blog

Discussion