Chrome DevToolsを使ったフロントエンドパフォーマンス改善
TECH
2023.04.24
更新日:2023.06.28
公開日:2023.06.30
エンジニアの田川です。
インフルエンサーマーケティングを支援するCast Me!というプロダクトでフロントエンド開発を担当しています。
アプリケーション開発において、スケールすることを想定した設計は非常に大切です。
私が入社した当初のCast Me!は、フロントエンドの設計方針などが定まっておらず、機能改修などをした際に意図せず他の機能が壊れてしまう問題が多々ありました。
直近では、新規開発と並行して新アーキテクチャへの刷新を行ってフロントエンドの設計の見直しやルールの策定などをしています。
今回は安全かつ素早く開発をできる状態にするために、Cast Me!で行っている取り組みを紹介します。
コードを変更する際、特定の機能に関連するコードが散乱していると、変更の影響を完全に理解するのが困難になり、結果としてバグを生む可能性が高まります。
1 2 3 4 5 |
src ├ apis/ ├ components/ ├ hooks/ └ utils/ |
1 2 3 4 5 6 7 8 9 |
src └ modules/ └ project/ ├ core/ ... 案件に関するコアモジュールはここに配置 │ ├ components/ │ └ hooks/ └ register/ ... 案件作成機能に関するモジュールはここに配置 ├ components/ └ hooks/ |
この構造にすることで、「案件作成機能」に関連するコードを変更したいと思った時には、「project/register」(及び「project/core」)を見るだけで、該当機能の全体像を把握できます。
また、機能ごとにディレクトリを分けているため、特定の機能を削除する際にはディレクトリごと削除することで、不要なコードの大部分を一括で削除できるなど、コードの管理がしやすくなります。
Cast Me!では、ドメインモデルを操作するロジックはEntityやValue Objectのファイルを別途作成し、そちらに記述するようにしています。
例えば、案件の手数料を計算するロジックは、下記のように、ProjectRewardのValue Objectに記載します。
1 2 3 4 5 6 7 8 |
// src/modules/project/core/domain/object/ProjectReward.ts import { Brand } from '@/types/util'; export type ProjectReward = Brand<number, 'ProjectReward'>; export const calcProjectRewardFee = (projectReward: ProjectReward) => ProjectReward { // 案件の手数料を計算するロジックを記述 } |
このアプローチにより、ロジックとUIが密結合になることを防ぎ、再利用可能でテストしやすいコードを実現できます。
また、案件の手数料に関連するロジックを探す際は、ProjectReward内を確認するだけで明確になります。
モジュールごとにAPI呼び出しや、ドメインモデルの定義、コンポーネントUIの定義などをそれぞれの層に分割するようにしています。
案件作成機能を例に挙げてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
src └ modules/ └ project ├ core/ │ └ domain/ │ ├ entities/ │ │ └ Project.ts │ │ │ └ objects/ │ └ ProjectId.ts └ register/ ... 案件作成機能に関するモジュールはここに配置 ├ components/ ├ domain/ │ └ entities/ │ └ DraftProject.ts ├ hooks/ │ ├ query │ │ └ useDraftProjectQuery/ │ └ mutation │ └ useUpdateDraftProjectMutation/ └ repositories/ ├ get │ └ findDraftProject/ └ put └ updateDraftProject/ |
この場合、処理の依存関係はrepositories → hooks → componentsとなります。
データの取得にはReact Queryを使用しています。
repository層はデータの取得に関心を持ち、データの加工やEntityへのマッピングはhooks層で行うようにします。domain層は前述の通りです。
このようにすることで、コンポーネントではUIに関連する処理のみを記述できるようになります。
TypeScriptで開発する際、以下の2つを使用することで型安全性を担保できるようにしています。
BrandedTypesを使用してプリミティブ型に固有の型を付与し、他のプリミティブ型の代入を防ぐようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type UserId = Brand<string, UserId>; type User = { userId: UserId; } const userName = 'user-1'; const user1: User = { userId: userName, // コンパイルエラー } const userId = 'user-1' as UserId; const user2: User = { userId: userId, } |
BrandedTypes自体の実装自体は、下記のようにします。
1 |
export type Brand<T, B> = T & { __brand: B }; |
Value Objectをclassで定義することでも代用可能ですが、valueへのアクセスが面倒なため、Cast Me!ではBrandedTypesでValue Objectを定義して利便性と型安全性を両立しています。
TypeScriptでは、enumを定義する機能があります。
1 2 3 4 |
export enum EMAIL_STATUS { NOT_VERIFY = 1, VERIFY = 2, } |
しかし、enumは意図しない値にアクセスできてしまう、ビルド時にTree-shakingが効かないなどいくつかの問題点があります。
そのためCast Me!では、代用としてconst assertionを使用しています。
1 2 3 4 5 |
export const EMAIL_STATUS = { NOT_VERIFY: 1, VERIFY: 2, } as const; export type EmailStatus = typeof EMAIL_STATUS[keyof typeof EMAIL_STATUS] // 1 | 2 |
ただし、この場合でも1か2であればどんな値でも代入できてしまうため、前述したBrandedTypesと組み合わせることでより安全にしています。
1 |
export type EmailStatus = Brand<(typeof EMAIL_STATUS)[keyof typeof EMAIL_STATUS], 'EmailStatus'>; |
このようにすると、EmailStatusを引数に受け取るような関数などで、他のプリミティブ値が入る可能性を防ぐことが可能になります。
フロントエンド開発においては、ベストプラクティスや新しい実装パターンが常に進化しています。しかし、一貫した設計手法を採用することで、リファクタリングや機能追加が容易になります。
常に学習と改善に取り組みながら、より良いフロントエンド開発を追求していきたいと思います。
PLAN-Bでは、この記事の内容以外にもフロントエンド技術の調査や導入、週に1度のフロントエンドに関する勉強会など、さまざまな取り組みを行っています。
もしモダンなフロントエンド開発に興味をお持ちの方がいらっしゃれば、ぜひ以下のリンクからご応募ください。