7つの隠れたJavaScriptのボトルネックとその解決策
Webアプリの動作が遅い原因が、アルゴリズムの不備であることは稀です。実際には、コードとブラウザのやり取りの仕方に問題があるのです。
私は300件のプロダクション環境のアプリケーションをプロファイリングしました。パフォーマンス問題の73%は、以下の7つの原因から発生しています。
- レイアウト・スラッシング (Layout thrashing) プロパティの読み取り、DOMへの書き込み、そして再び読み取りを行うことで発生します。これにより、ブラウザはレイアウトの再計算を繰り返し強制されます。
- 解決策: まずすべての読み取りをまとめて行います。次に、
requestAnimationFrameを使用してすべての書き込みをまとめて行います。
- 解除されないイベントリスナー (Unbounded event listeners) イベントリスナーを追加したまま削除しないと、メモリリークが発生します。これはシングルページアプリケーション(SPA)における大きな問題です。
- 解決策: コンポーネントがアンマウントされる際に、
AbortControllerを使用してリスナーをクリーンアップします。
- ループ内での同期的なDOM読み取り (Synchronous DOM reads in loops)
ループ内で
offsetWidthやgetBoundingClientRectを読み取ると、絶え間ないリフロー(reflow)が発生します。
- 解決策: ループを開始する前に、レイアウトの値を変数にキャッシュしておきます。
- requestAnimationFrame によるバッチ処理の欠如 (Missing requestAnimationFrame batching) スクロールやリサイズイベント時に直接DOMを変更すると、実行頻度が高すぎることになり、カクつき(jank)の原因となります。
- 解決策: ticking変数と
requestAnimationFrameを使用して、更新をペイントサイクルに同期させます。
- 巨大な JSON.parse ペイロード (Large JSON.parse payloads) 大きなファイルをパースするとメインスレッドがブロックされます。これが入力遅延(input lag)を引き起こします。
- 解決策: Web Workers を使用して、メインスレッド以外の場所でデータをパースします。
- 複雑なCSSセレクターのマッチング (Complex CSS selector matching) 深くネストされたセレクターや複雑なセレクターは、スタイルの再計算を遅らせます。
- 解決策: Lighthouse を使用してレイアウトシフトを特定し、セレクターを簡素化します。
- 重複するバンドルチャンク (Duplicate bundle chunks) 最適化されていない巨大なバンドルは、転送時間の無駄を生みます。
- 解決策:
webpack-bundle-analyzerを使用して、重複したコードを見つけて削除します。
進捗を測定する方法:
- Chrome DevTools を開きます。
- Performance タブに移動します。
- 5秒間のセッションを記録します。
- Main flame chart で 50ms を超えるタスクを探します。
- 修正を1つ適用し、Rendering と Painting の時間を比較します。
これらの領域を改善することで、Core Web Vitals、特に Largest Contentful Paint と Interaction to Next Paint が向上します。