Supabseチームはどのようにフロントエンドの高速化に成功したのか
ブログからのクロス投稿です。
この記事は、Supabaseチームによる記事「Making the Supabase Dashboard Supa-fast」の和訳記事です。
Supabaseのダッシュボードは、この1ヶ月でより機能が充実しました。Monacoによる強力なSQLエディタを用意しました。私たちは、データベースのAirtableのようなビューを構築し、編集が簡単になりました。
機能・性能・DX-3つの選択
特にシングルページアプリケーションでは、新しい機能を追加すると、パフォーマンスがすぐに低下することがあります。ここでは、開発者の体験(DX)を損なうことなく、アプリケーション内で良好なベースラインパフォーマンスを保証するために行ったステップを紹介します。
ベースラインを確立し、目標を設定する。
測定できないものは直せない
パフォーマンスを向上させるために、容易に解決できる問題がいくつかありましたが、その前に1つ重要なことがありました。
私たちのダッシュボードはJavaScriptが重いので、バンドルサイズを追跡するためにアナリティクスを設定することから始めました。 Next-bundle-analyzer (または webpack-bundle-analyzer) は、生成された JavaScript バンドルのインタラクティブなツリーマップを提供してくれます。これは私たちの当初のツリーマップです。最も影響の大きい変更点を明確に示してくれました。
リアルユーザーモニタリング(RUM)に関しては、いくつかの素晴らしいツールがあります。私たちは、すでにエラートラッキングにSentryを使用しており、スタック内の新しいツールを最小限に抑えたいと考えていたので、新しく発売されたSentry performance monitoring製品を選択しました。Sentry performance monitoringは、Googleが作成したパフォーマンスメトリクスである「Core Web Vitals」(https://web.dev/vitals/)のレポート機能にも対応しており、初期読み込み性能、応答性、視覚的安定性をトラッキングすることができます。Core Web Vitalsには推奨目標値が記載されているので、明確な目標を達成することができます。
JavaScript のバンドルサイズの改善
ユーザーのブラウザに npm レジストリ全体をロードしないようにする方法
小さいモジュールの選択
最大のモジュールにはBundlephobiaを使用しました。これはあなたの対JSパフォーマンス用武器庫に入れておくと便利なウェブサイトです。異なるバージョン間での npm モジュールのサイズを表示し、似たような機能を持つ、より小さい代替モジュールを推奨してくれます。
Moment.js
はバンドルサイズが大きいことで有名ですが、私たちのダッシュボードには複雑な日付処理は必要ありません。Moment.js
と API 互換性の高い day-js に切り替えるのは簡単でした。この変更により、バンドルのサイズが68KB減少しました。
スキーマ検証のために Joi
から ajv
に移行しました。 ajv` は他のモジュールの依存関係として既にバンドルされていたので、非常に簡単でした。
crypto-jsモジュールをバージョン4.0から3.3.0に戻しました。バージョン4.0では、ブラウザのコンテキストで使用すると、400kb以上のコードが注入されます。新しいバージョンでは、Math.random
をノードの実装に置き換え、ノードの暗号化モジュール全体をブラウザのコンテキストに注入しています。ユーザーのAPIキーの復号化には crypto-js
を利用しているので、PRNGのランダム性には依存していません。将来的には aes-js のような専用モジュールに移行するかもしれません。
部分的にimportする
lodash
のようなモジュールから関数を選択的にインポートすることで、さらに圧縮サイズを40kb削減しました。
// before
import _ from 'lodash'
// maunally cherry picking modules
import find from 'lodash/find'
import debounce from 'lodash/debounce'
// using babel-plugin-lodash
import { find, debounce } from 'lodash'
上記の例では、babel-plugin-lodash を babel の設定に追加して、インポートする lodash
関数を正確に選択しています。これにより、選択的なインポート文でコードを混乱させることなく、lodash
からのインポートが容易になります。
複雑なロジックをサーバーに移す
熟練したハクサー(というか、主に弱いパスワード)のおかげで、顧客のデータベースの一部では暗号採掘機が稼働していました。これを防ぐために、私たちは zxcvbn モジュールを使ってパスワードの強度を強化しました。これにより全体的なセキュリティが向上しましたが、このモジュールは かなり大きく 、388kb の圧縮された重さがあります。これを回避するために、パスワード強度チェックのロジックを API に移動しました。これで、フロントエンドがユーザが提供したパスワードをサーバに問い合わせ、サーバがその強度を計算するようになりました。これにより、フロントエンドからモジュールを削除することができます。
Lazy loading code
xlsxもまた複雑で大規模なモジュールで、スプレッドシートをテーブルにインポートするために使用されます。このロジックをバックエンドに移すことも考えましたが、別の解決策を見つけました。
スプレッドシートのインポートは、ユーザーが新しいテーブルを作成するときにトリガーされます。しかし、以前は、新しいテーブルが作成されていなくても、ページが訪問されるたびにコードがロードされていました。そのため、遅延読み込みの格好の候補になっていました。Next.js dynamic importsを使用して、ユーザーが「コンテンツの追加」ボタンをクリックするたびに、このコンポーネント(313kb)を動的にロードすることができます。
YouTubeのvideoIDが不正です
ネイティブブラウザのAPIを使用する
私たちは IE11 をサポートしないことを決定し、最適化のためのより多くの道を開きました。ネイティブブラウザの API を使用することで、より多くの依存関係を削除することができました。例えば、気になるすべてのブラウザで fetch API が利用可能なので、axios を削除して、ネイティブの fetch API を使ったシンプルなラッパーを構築しました。
Vercel のデフォルトキャッシュの改善
最速のリクエストは、リクエストをしないこと
Vercel が Cache-Control
ヘッダに public, max-age=0, must-revalidate
を送信していたため、SVG、CSS、およびフォントファイルの一部がブラウザでキャッシュされないことに気づきました。
今回、next.config.js
を更新し、Vercelが送信するキャッシングヘッダに長いmax-age
を追加しました。私たちのアセットは適切にバージョン管理されているので、これを安全に行うことができました。
Next.jsの自動静的最適化を有効にする
Next.jsでは、ある条件を満たすページがあると、自動的にHTMLにプリレンダリングすることができます。このモードはAutomatic Static Optimizationと呼ばれています。プリレンダリングされたページはCDNにキャッシュされ、ページの読み込みが非常に速くなります。このモードを利用するために、getServerSideProps
と getInitialProps
の呼び出しを削除しました。
パフォーマンス文化の開発
いつも目の前にあり、いつも心の中にあります
当社のパフォーマンス最適化の旅は決して完全なものではありません。ユーザー全体のベースラインを維持するためには、常に注意を払う必要があります。これをチーム内に浸透させるために、私たちはいくつかのアクションを取りました。
Slackボットを開発し、Sentryのパフォーマンス・ダッシュボードを毎週送信し、最も遅いトランザクションとCore Web Vitalsのサマリーが含まれています。これにより、どのページに改善の必要があるのか、また、ユーザーが最も悲惨な状況にある場所がわかります。
アルファ版からベータ版への移行の間、パフォーマンスは安定性とセキュリティと共に重要な機能の一つでした。私たちは、ライブラリやツールを選択する際に、パフォーマンスへの影響を考慮しました。このような議論の中で話をすることで、パフォーマンスが後回しにされないようにしています。
結果
これらの変更により、Core Web Vitalsのスコアは立派なものになりました。これは先週のRUMデータを使ったSentryからのスナップショットです。3つのWeb Vitalsすべてにおいて、推奨されている閾値内に収まっています。
Next.js のビルド出力では、ユーザーが 2 つのページ遷移の間に 200 kb 未満の JavaScript をダウンロードしていることが示されています。ダッシュボードでは多くの機能を提供していますが、今後もバンドルサイズを縮小していきます。
うまくいかなかったこと
勝つこともあるし負けることもある
Import costというVSCodeプラグインを試してみました。これは、エディタ上でJavaScriptモジュールをインポートしたときのサイズを表示してくれます。しかし、このプラグインは私たちのコードベースでは動作しませんでした。なぜなら、このプラグインはオプションのチェーニングのようないくつかの JavaScript 機能をサポートしていないからです。
また、lodash-webpack-pluginはJavaScriptのサイズを小さくできる可能性があったにもかかわらず、使用を見送りました。このプラグインは、フロントエンドチームが Webpack の設定を理解し、新しいlodash機能セットを使うたびにそれを更新する必要があります。
♪ これからの道のり
私たちの大きな目標は、フロントエンドのパフォーマンスのベストプラクティスを実装し、チーム全員がワクワクできるようにすることです。以下は、私たちのロードマップにあるいくつかのアイデアです。
- Github ActionでLighthouseを設定し、開発ライフサイクルの早い段階でパフォーマンスの低下をキャッチします。
- LCPの時間を改善するために、JavaScriptの初期ペイロードサイズの削減を継続します。
- ブラウザ上でサードパーティのライブラリをロードする代わりに、サーバからAPIコールを行う Segment の
cloud-mode
を検討する。
ウェブサイトを高速化するためのアイデアがあれば、Twitterまでご連絡ください。
Discussion