29 KiB
29 KiB
| applyTo | description |
|---|---|
| * | 全ての言語、フレームワーク、スタック向けの最も包括的で実践的なパフォーマンス最適化の指針。フロントエンド、バックエンド、データベースのベストプラクティスを、実行可能なガイダンス、シナリオベースのチェックリスト、トラブルシューティング、およびプロのヒントと共にカバー。 |
パフォーマンス最適化のベストプラクティス
はじめに
パフォーマンスは単なる流行語ではありません。それは人々に愛される製品と放棄される製品の違いです。遅いアプリがユーザーを苛立たせ、クラウドの請求書を膨らませ、顧客を失わせることを私は直接見てきました。このガイドは、フロントエンド、バックエンド、データベース層、および高度なトピックをカバーする、私が使用し、レビューしてきた最も効果的で実世界のパフォーマンス実践の生きたコレクションです。これを参考書、チェックリスト、そして高速で効率的でスケーラブルなソフトウェアを構築するためのインスピレーションのソースとして使用してください。
一般原則
- まず測定、次に最適化: 最適化の前に常にプロファイルと測定を行う。ベンチマーク、プロファイラー、監視ツールを使用して実際のボトルネックを特定する。推測はパフォーマンスの敵である。
- プロのヒント: Chrome DevTools、Lighthouse、New Relic、Datadog、Py-Spy、または使用言語の組み込みプロファイラーなどのツールを使用する。
- 一般的なケースを最適化: 最も頻繁に実行されるコードパスの最適化に焦点を当てる。クリティカルでない限り、稀なエッジケースに時間を無駄にしない。
- 早すぎる最適化を避ける: まず明確で保守可能なコードを書き、必要な時にのみ最適化する。早すぎる最適化はコードを読みにくく保守しにくくする可能性がある。
- リソース使用量を最小化: メモリ、CPU、ネットワーク、ディスクリソースを効率的に使用する。常に「これをより少ないリソースで実行できるか?」を問う。
- シンプルさを好む: シンプルなアルゴリズムとデータ構造は、しばしばより高速で最適化しやすい。過度に工学的にしない。
- パフォーマンス前提を文書化: パフォーマンスクリティカルまたは自明でない最適化を持つコードに明確にコメントする。将来の保守者(あなた自身を含む)が感謝するだろう。
- プラットフォームを理解: 使用言語、フレームワーク、ランタイムのパフォーマンス特性を知る。Python で高速なものは JavaScript で遅い可能性があり、その逆もある。
- パフォーマンステストを自動化: CI/CD パイプラインにパフォーマンステストとベンチマークを統合する。リグレッションを早期に発見する。
- パフォーマンス予算を設定: 読み込み時間、メモリ使用量、API レイテンシなどの許容限界を定義する。自動チェックで強制する。
フロントエンドパフォーマンス
レンダリングと DOM
- DOM 操作を最小化: 可能な場合は更新をバッチ処理する。頻繁な DOM 変更は高コストである。
- アンチパターン: ループ内で DOM を更新する。代わりにドキュメントフラグメントを構築して一度に追加する。
- 仮想 DOM フレームワーク: React、Vue、または類似のものを効率的に使用し、不要な再レンダリングを避ける。
- React 例:
React.memo、useMemo、useCallbackを使用して不要なレンダリングを防ぐ。
- React 例:
- リスト内のキー: 仮想 DOM の差分計算を助けるため、リストでは常に安定したキーを使用する。リストが静的でない限り、配列インデックスをキーとして使用することを避ける。
- インラインスタイルを避ける: インラインスタイルはレイアウトスラッシングを引き起こす可能性がある。CSS クラスを好む。
- CSS アニメーション: よりスムーズで GPU アクセラレーションされた効果のために、JavaScript よりも CSS 遷移/アニメーションを使用する。
- 非クリティカルレンダリングの遅延:
requestIdleCallbackまたは類似のものを使用して、ブラウザがアイドル状態の時まで作業を遅延する。
アセット最適化
- 画像圧縮: ImageOptim、Squoosh、TinyPNG などのツールを使用する。Web 配信には現代的なフォーマット(WebP、AVIF)を好む。
- アイコン用の SVG: SVG はうまくスケールし、シンプルなグラフィックスではしばしば PNG よりも小さい。
- 圧縮とバンドル: Webpack、Rollup、esbuild を使用して JS/CSS をバンドルし圧縮する。デッドコードを削除するためにツリーシェイキングを有効にする。
- キャッシュヘッダー: 静的アセットに長期間のキャッシュヘッダーを設定する。更新時にはキャッシュバスティングを使用する。
- 遅延読み込み: 画像には
loading="lazy"を使用し、JS モジュール/コンポーネントには動的インポートを使用する。 - フォント最適化: 必要な文字セットのみを使用する。フォントをサブセット化し、
font-display: swapを使用する。
ネットワーク最適化
- HTTP リクエストを削減: ファイルを結合し、イメージスプライトを使用し、クリティカル CSS をインライン化する。
- HTTP/2 と HTTP/3: 多重化と低レイテンシのためにこれらのプロトコルを有効にする。
- クライアントサイドキャッシング: オフラインと再訪問のために Service Workers、IndexedDB、localStorage を使用する。
- CDN: ユーザーに近い CDN から静的アセットを提供する。冗長性のために複数の CDN を使用する。
- Defer/Async スクリプト: レンダリングのブロッキングを避けるために、非クリティカル JS に
deferやasyncを使用する。 - プリロードとプリフェッチ: クリティカルリソースに
<link rel="preload">と<link rel="prefetch">を使用する。
JavaScript パフォーマンス
- メインスレッドのブロッキングを避ける: 重い計算を Web Workers にオフロードする。
- イベントのデバウンス/スロットル: スクロール、リサイズ、入力イベントには、ハンドラー頻度を制限するためにデバウンス/スロットルを使用する。
- メモリリーク: イベントリスナー、インターバル、DOM 参照をクリーンアップする。分離されたノードをチェックするためにブラウザ開発ツールを使用する。
- 効率的なデータ構造: ルックアップには Maps/Sets、数値データには TypedArrays を使用する。
- グローバル変数を避ける: グローバルはメモリリークと予測不能なパフォーマンスを引き起こす可能性がある。
- 深いオブジェクトクローンを避ける: 浅いコピーまたは必要な時のみ lodash の
cloneDeepのようなライブラリを使用する。
アクセシビリティとパフォーマンス
- アクセシブルコンポーネント: ARIA 更新が過度でないことを確実にする。アクセシビリティとパフォーマンスの両方にセマンティック HTML を使用する。
- スクリーンリーダーパフォーマンス: 支援技術を圧倒する可能性のある急激な DOM 更新を避ける。
フレームワーク固有のヒント
React
- 不要なレンダリングを避けるために
React.memo、useMemo、useCallbackを使用する。 - 大きなコンポーネントを分割し、コード分割(
React.lazy、Suspense)を使用する。 - レンダー内で匿名関数を避ける;それらは毎回のレンダーで新しい参照を作成する。
- エラーを優雅に捕捉し処理するために
ErrorBoundaryを使用する。 - React DevTools プロファイラーでプロファイルする。
Angular
- 頻繁な更新が不要なコンポーネントには OnPush 変更検出を使用する。
- テンプレート内の複雑な式を避ける;ロジックをコンポーネントクラスに移す。
- 効率的なリストレンダリングのために
ngForでtrackByを使用する。 - Angular Router でモジュールとコンポーネントを遅延読み込みする。
- Angular DevTools でプロファイルする。
Vue
- キャッシングのためにテンプレート内でメソッドよりも算出プロパティを使用する。
v-show対v-ifを適切に使用する(v-showは頻繁な表示切り替えに適している)。- Vue Router でコンポーネントとルートを遅延読み込みする。
- Vue Devtools でプロファイルする。
一般的なフロントエンドの落とし穴
- 初期ページ読み込み時の大きな JS バンドルの読み込み。
- 画像の圧縮なしまたは時代遅れのフォーマットの使用。
- イベントリスナーのクリーンアップの失敗によるメモリリーク。
- シンプルなタスクでのサードパーティライブラリの過度の使用。
- モバイルパフォーマンスの無視(実際のデバイスでテスト!)。
フロントエンドトラブルシューティング
- Chrome DevTools のパフォーマンスタブを使用して遅いフレームを記録し分析する。
- Lighthouse を使用してパフォーマンスを監査し実行可能な提案を得る。
- WebPageTest を使用して実世界の負荷テストを行う。
- ユーザー中心のメトリクス(LCP、FID、CLS)のコア Web バイタルを監視する。
バックエンドパフォーマンス
アルゴリズムとデータ構造の最適化
- 適切なデータ構造を選択: 順次アクセスには配列、高速ルックアップにはハッシュマップ、階層データにはツリーなど。
- 効率的なアルゴリズム: 適切な場合にはバイナリサーチ、クイックソート、ハッシュベースのアルゴリズムを使用する。
- O(n^2)以上を避ける: ネストしたループと再帰呼び出しをプロファイルする。複雑さを減らすためにリファクタリングする。
- バッチ処理: オーバーヘッドを削減するためにデータをバッチで処理する(例:一括データベース挿入)。
- ストリーミング: すべてをメモリに読み込むことを避けるために、大きなデータセットにはストリーミング API を使用する。
並行性と並列性
- 非同期 I/O: スレッドのブロッキングを避けるために async/await、コールバック、イベントループを使用する。
- スレッド/ワーカープール: 並行性を管理しリソース枯渇を避けるためにプールを使用する。
- 競合状態を避ける: 必要に応じてロック、セマフォ、アトミック操作を使用する。
- 一括操作: ラウンドトリップを削減するためにネットワーク/データベース呼び出しをバッチ処理する。
- バックプレッシャー: 過負荷を避けるためにキューとパイプラインにバックプレッシャーを実装する。
キャッシング
- 高コストな計算のキャッシュ: ホットデータにはインメモリキャッシュ(Redis、Memcached)を使用する。
- キャッシュ無効化: 時間ベース(TTL)、イベントベース、または手動無効化を使用する。古いキャッシュはキャッシュなしより悪い。
- 分散キャッシング: マルチサーバーセットアップには分散キャッシュを使用し、一貫性の問題を認識する。
- キャッシュ踏み荒らし保護: サンダリングハードの問題を防ぐためにロックまたはリクエスト合体を使用する。
- すべてをキャッシュしない: 一部のデータは揮発性すぎるかセンシティブすぎてキャッシュできない。
API とネットワーク
- ペイロードを最小化: JSON を使用し、応答を圧縮(gzip、Brotli)し、不要なデータの送信を避ける。
- ページング: 大きな結果セットは常にページ分割する。リアルタイムデータにはカーソルを使用する。
- レート制限: 乱用と過負荷から API を保護する。
- コネクションプーリング: データベースと外部サービスの接続を再利用する。
- プロトコル選択: 高スループット、低レイテンシ通信には HTTP/2、gRPC、WebSockets を使用する。
ログと監視
- ホットパスでのログを最小化: 過度なログは重要なコードを遅くする可能性がある。
- 構造化ログ: 解析と分析を容易にするために JSON またはキーバリューログを使用する。
- すべてを監視: レイテンシ、スループット、エラー率、リソース使用量。Prometheus、Grafana、Datadog などを使用する。
- アラート: パフォーマンスリグレッションとリソース枯渇のアラートを設定する。
言語/フレームワーク固有のヒント
Node.js
- 非同期 API を使用;イベントループのブロッキングを避ける(例:本番で
fs.readFileSyncを使用しない)。 - CPU 集約的なタスクにはクラスタリングまたはワーカースレッドを使用する。
- リソース枯渇を避けるために同時オープン接続を制限する。
- 大きなファイルまたはネットワークデータ処理にはストリームを使用する。
clinic.js、node --inspect、Chrome DevTools でプロファイルする。
Python
- 速度のために組み込みデータ構造(
dict、set、deque)を使用する。 cProfile、line_profiler、Py-Spyでプロファイルする。- 並列性には
multiprocessingまたはasyncioを使用する。 - CPU 集約的なコードで GIL ボトルネックを避ける;C 拡張またはサブプロセスを使用する。
- メモ化には
lru_cacheを使用する。
Java
- 効率的なコレクション(
ArrayList、HashMapなど)を使用する。 - VisualVM、JProfiler、YourKit でプロファイルする。
- 並行性にはスレッドプール(
Executors)を使用する。 - ヒープとガベージコレクション(
-Xmx、-Xms、-XX:+UseG1GC)の JVM オプションを調整する。 - 非同期プログラミングには
CompletableFutureを使用する。
.NET
- I/O バウンド操作には
async/awaitを使用する。 - 効率的なメモリアクセスには
Span<T>とMemory<T>を使用する。 - dotTrace、Visual Studio Profiler、PerfView でプロファイルする。
- 適切な場合にオブジェクトと接続をプールする。
- ストリーミングデータには
IAsyncEnumerable<T>を使用する。
一般的なバックエンドの落とし穴
- Web サーバーでの同期/ブロッキング I/O。
- データベースでコネクションプーリングを使用しない。
- 過度なキャッシングまたはセンシティブ/揮発性データのキャッシュ。
- 非同期コードでのエラーハンドリングの無視。
- パフォーマンスリグレッションの監視またはアラートなし。
バックエンドトラブルシューティング
- フレームグラフを使用して CPU 使用量を可視化する。
- 分散トレーシング(OpenTelemetry、Jaeger、Zipkin)を使用してサービス間のリクエストレイテンシを追跡する。
- ヒープダンプとメモリプロファイラーを使用してリークを見つける。
- 遅いクエリと API 呼び出しをログに記録して分析する。
データベースパフォーマンス
クエリ最適化
- インデックス: 頻繁にクエリ、フィルタ、結合される列にインデックスを使用する。インデックス使用量を監視し、未使用のインデックスを削除する。
- SELECT *を避ける: 必要な列のみを選択する。I/O とメモリ使用量を削減する。
- パラメータ化クエリ: SQL インジェクションを防ぎ、プランキャッシングを改善する。
- クエリプラン: クエリ実行プランを分析し最適化する。SQL データベースで
EXPLAINを使用する。 - N+1 クエリを避ける: ループでの繰り返しクエリを避けるために結合またはバッチクエリを使用する。
- 結果セットを制限: 大きなテーブルには
LIMIT/OFFSETまたはカーソルを使用する。
スキーマ設計
- 正規化: 冗長性を削減するために正規化するが、必要に応じて読み込み集約的なワークロードには非正規化する。
- データ型: 最も効率的なデータ型を使用し、適切な制約を設定する。
- パーティショニング: スケーラビリティと管理性のために大きなテーブルをパーティション化する。
- アーカイブ: テーブルを小さく高速に保つために古いデータを定期的にアーカイブまたはパージする。
- 外部キー: データ整合性のために使用するが、高書き込みシナリオでのパフォーマンストレードオフを認識する。
トランザクション
- 短いトランザクション: ロック競合を削減するためにトランザクションを可能な限り短く保つ。
- 分離レベル: 一貫性ニーズを満たす最低の分離レベルを使用する。
- 長時間実行されるトランザクションを避ける: それらは他の操作をブロックし、デッドロックを増加させる可能性がある。
キャッシングとレプリケーション
- 読み取りレプリカ: 読み込み集約的なワークロードのスケーリングに使用する。レプリケーション遅延を監視する。
- クエリ結果のキャッシュ: 頻繁にアクセスされるクエリに Redis または Memcached を使用する。
- Write-Through/Write-Behind: 一貫性ニーズに適した戦略を選択する。
- シャーディング: スケーラビリティのために複数のサーバーにデータを分散する。
NoSQL データベース
- アクセスパターンのために設計: 必要なクエリのためにデータをモデル化する。
- ホットパーティションを避ける: 書き込み/読み取りを均等に分散する。
- 制限のない成長: 制限のない配列やドキュメントに注意する。
- シャーディングとレプリケーション: スケーラビリティと可用性のために使用する。
- 一貫性モデル: 結果的一貫性対強い一貫性を理解し、適切に選択する。
一般的なデータベースの落とし穴
- 不足または未使用のインデックス。
- 本番クエリでの SELECT *。
- 遅いクエリの監視なし。
- レプリケーション遅延の無視。
- 古いデータのアーカイブなし。
データベーストラブルシューティング
- 遅いクエリログを使用してボトルネックを特定する。
EXPLAINを使用してクエリプランを分析する。- キャッシュヒット/ミス比を監視する。
- データベース固有の監視ツール(pg_stat_statements、MySQL Performance Schema)を使用する。
パフォーマンスのためのコードレビューチェックリスト
- 明らかなアルゴリズムの非効率性(O(n^2)以上)はあるか?
- データ構造はその使用に適しているか?
- 不要な計算や繰り返し作業はあるか?
- 適切な場合にキャッシングが使用され、無効化が正しく処理されているか?
- データベースクエリは最適化され、インデックス化され、N+1 問題がないか?
- 大きなペイロードはページング、ストリーミング、またはチャンク化されているか?
- メモリリークや制限のないリソース使用はあるか?
- ネットワークリクエストは最小化、バッチ化され、失敗時に再試行されるか?
- アセットは最適化、圧縮され、効率的に提供されているか?
- ホットパスにブロッキング操作はあるか?
- ホットパスでのログは最小化され構造化されているか?
- パフォーマンスクリティカルなコードパスは文書化されテストされているか?
- パフォーマンスセンシティブなコードに自動テストまたはベンチマークはあるか?
- パフォーマンスリグレッションのアラートはあるか?
- アンチパターン(例:SELECT *、ブロッキング I/O、グローバル変数)はあるか?
高度なトピック
プロファイリングとベンチマーキング
- プロファイラー: ボトルネックを特定するために言語固有のプロファイラー(Chrome DevTools、Py-Spy、VisualVM、dotTrace など)を使用する。
- マイクロベンチマーク: クリティカルなコードパスにマイクロベンチマークを書く。
benchmark.js、pytest-benchmark、Java の JMH を使用する。 - A/B テスト: A/B またはカナリアリリースで最適化の実世界への影響を測定する。
- 継続的パフォーマンステスト: CI/CD にパフォーマンステストを統合する。k6、Gatling、Locust などのツールを使用する。
メモリ管理
- リソースクリーンアップ: リソース(ファイル、ソケット、DB 接続)を常に迅速に解放する。
- オブジェクトプーリング: 頻繁に作成/破棄されるオブジェクト(例:DB 接続、スレッド)に使用する。
- ヒープ監視: ヒープ使用量とガベージコレクションを監視する。ワークロードに対して GC 設定を調整する。
- メモリリーク: リーク検出ツール(Valgrind、LeakCanary、Chrome DevTools)を使用する。
スケーラビリティ
- 水平スケーリング: ステートレスサービスを設計し、シャーディング/パーティショニング、ロードバランサーを使用する。
- オートスケーリング: クラウドオートスケーリンググループを使用し、適切なしきい値を設定する。
- ボトルネック分析: 単一障害点を特定し対処する。
- 分散システム: 冪等操作、再試行、サーキットブレーカーを使用する。
セキュリティとパフォーマンス
- 効率的な暗号化: ハードウェアアクセラレーションされた、よく保守された暗号ライブラリを使用する。
- 検証: 入力を効率的に検証;ホットパスでの正規表現を避ける。
- レート制限: 正当なユーザーに害を与えることなく DoS から保護する。
モバイルパフォーマンス
- 起動時間: 機能を遅延読み込みし、重い作業を遅延し、初期バンドルサイズを最小化する。
- 画像/アセット最適化: レスポンシブ画像を使用し、モバイル帯域幅のためにアセットを圧縮する。
- 効率的なストレージ: SQLite、Realm、プラットフォーム最適化されたストレージを使用する。
- プロファイリング: Android Profiler、Instruments(iOS)、Firebase Performance Monitoring を使用する。
クラウドとサーバーレス
- コールドスタート: 依存関係を最小化し、関数を温める。
- リソース割り当て: サーバーレス関数のメモリ/CPU を調整する。
- 管理されたサービス: スケーラビリティのために管理されたキャッシング、キュー、DB を使用する。
- コスト最適化: パフォーマンス指標としてクラウドコストを監視し最適化する。
実践的な例
例 1:JavaScript でのユーザー入力のデバウンス
// 悪い:キーストロークごとにAPI呼び出しをトリガー
input.addEventListener("input", (e) => {
fetch(`/search?q=${e.target.value}`);
});
// 良い:API呼び出しをデバウンス
let timeout;
input.addEventListener("input", (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
fetch(`/search?q=${e.target.value}`);
}, 300);
});
例 2:効率的な SQL クエリ
-- 悪い:すべての列を選択し、インデックスを使用しない
SELECT * FROM users WHERE email = 'user@example.com';
-- 良い:必要な列のみを選択し、インデックスを使用
SELECT id, name FROM users WHERE email = 'user@example.com';
例 3:Python での高コスト計算のキャッシング
# 悪い:毎回結果を再計算
result = expensive_function(x)
# 良い:結果をキャッシュ
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_function(x):
...
result = expensive_function(x)
例 4:HTML での画像遅延読み込み
<!-- 悪い:すべての画像をすぐに読み込み -->
<img src="large-image.jpg" />
<!-- 良い:画像を遅延読み込み -->
<img src="large-image.jpg" loading="lazy" />
例 5:Node.js での非同期 I/O
// 悪い:ブロッキングファイル読み取り
const data = fs.readFileSync("file.txt");
// 良い:ノンブロッキングファイル読み取り
fs.readFile("file.txt", (err, data) => {
if (err) throw err;
// データを処理
});
例 6:Python 関数のプロファイリング
import cProfile
import pstats
def slow_function():
...
cProfile.run('slow_function()', 'profile.stats')
p = pstats.Stats('profile.stats')
p.sort_stats('cumulative').print_stats(10)
例 7:Node.js での Redis キャッシング使用
const redis = require("redis");
const client = redis.createClient();
function getCachedData(key, fetchFunction) {
return new Promise((resolve, reject) => {
client.get(key, (err, data) => {
if (data) return resolve(JSON.parse(data));
fetchFunction().then((result) => {
client.setex(key, 3600, JSON.stringify(result));
resolve(result);
});
});
});
}
参考文献と追加読み物
- Google Web Fundamentals: Performance
- MDN Web Docs: Performance
- OWASP: Performance Testing
- Microsoft Performance Best Practices
- PostgreSQL Performance Optimization
- MySQL Performance Tuning
- Node.js Performance Best Practices
- Python Performance Tips
- Java Performance Tuning
- .NET Performance Guide
- WebPageTest
- Lighthouse
- Prometheus
- Grafana
- k6 Load Testing
- Gatling
- Locust
- OpenTelemetry
- Jaeger
- Zipkin
まとめ
パフォーマンス最適化は継続的なプロセスです。常に測定し、プロファイルし、反復してください。これらのベストプラクティス、チェックリスト、トラブルシューティングのヒントを使用して、高パフォーマンスで、スケーラブルで、効率的なソフトウェアの開発とコードレビューを指導してください。新しいヒントや学んだ教訓があれば、ここに追加してください。このガイドを成長させ続けましょう!