Lighthouseで満点を獲得するためのすべての最適化手法

私は自分のサイトで常にLighthouseを実行しています。ローカルでの実行では常に100を維持しており、VercelのReal Experience Scoreでも100を記録しています。

これらのスコアは、一般的なチェックリストに従っただけのものではありません。ブラウザで行うべき処理をビルドステージへと移すことで、このスコアを実現しています。

その具体的な方法を以下に紹介します。

処理をビルド時に移す

多くのガイドでは、あらゆるものをレイジーロード(遅延読み込み)するように推奨されます。しかし、私はプリレンダリングを好みます。具体的には、プリレンダリングを有効にしたTanStack Startを使用しています。

これにより、ビルド中にサイト全体が静的なHTMLに変換されます。ブラウザは、最初のページを表示するためだけに重いJavaScriptを実行する必要がありません。ユーザーがEnterキーを押したときには、すでにHTMLが存在している状態になります。

複雑なロジックを事前計算する

私のホームページには、5,000個のドットがある世界地図があります。通常、ライブラリはGeoJSONを解析し、メインスレッド上で計算を実行します。これにより、ページが1,000ms間ブロックされてしまいます。

私は、この計算処理をビルドスクリプトに移すことで解決しました。

  • 5,000個のドットすべてに対して、単一のSVG path文字列を生成します。
  • 5,000個の個別の円をレンダリングするよりも、1つのパスとしてレンダリングする方がブラウザにとっては遥かに高速です。
  • 座標のルックアップテーブルを事前計算しておくことで、実行時にブラウザが行う計算をゼロにしています。

1,000msの遅延は、一瞬での描画(paint)へと変わります。

フォント読み込みの最適化

メインのフォントには rel="preload" を使用しています。

よくある間違いは、crossOrigin 属性を忘れてしまうことです。これを省略すると、ブラウザはフォントを2回取得してしまいます。これがLargest Contentful Paint (LCP) を悪化させる原因となります。私は、ファーストビュー(above the fold)で使用される3つのフォントのみをプリロードしています。

アニメーションに適切なツールを使う

マップマーカーの単純なパルスアニメーションにはSMILを使用しています。これは、Reactのstateを使ってアニメーションループを制御するよりも負荷が低いです。これにより、ブラウザはコンポジタースレッドで処理を行うことができます。

複雑なパスにはmotionを使用していますが、使い方はシンプルに留めています。マウント時に一度だけアニメーションさせ、スクロール位置の監視は避けています。

ベクターとWebPに絞る

ロゴや図形であればSVGを使用します。写真であればWebPを使用します。これにより、ファイルサイズを低く抑え、レイアウトシフトを防ぐことができます。

オーバーエンジニアリングを避ける

画像CDNは使用していません。複雑なコード分割も行っていません。私のサイトは規模が小さいため、ルートレベルの分割だけで十分です。

満点は単なる虚栄の指標(vanity metric)になり得ます。真の目的は、パフォーマンスを測定し、可能な限り多くの処理をユーザーのデバイスから遠ざけることです。

私のポートフォリオ: brodin.dev

ソースコード: github.com/NathanBrodin/Portfolio

TanStack Startのプリレンダリング: tanstack.com/start

Paper Shaders: shaders.paper.design

記事全文: https://dev.to/nathan-brodin/every-optimization-behind-a-perfect-lighthouse-score-283n