記事一覧に戻る
Supabaseの認証が遅い? getUser を getClaims に変えたら40倍速くなった話

Supabaseの認証が遅い? getUser を getClaims に変えたら40倍速くなった話

Supabase Auth の getUser が毎回80ms以上かかっていた問題を、getClaims に変更することで劇的に改善した実践記録。JWTのローカル検証で40倍速くなった計測結果を共有します。

Sentryでトレースを眺めていたら、認証処理だけで80ms以上かかっていることに気づきました。

変更前:getUser が 87.99ms かかっている

「え、ユーザー情報を取得するだけでこんなにかかるの?」と思って調べてみたら、Supabaseの getUser メソッドの仕様が原因でした。

何が起きていたか

バックエンドのAPIでは、リクエストごとにJWTトークンを検証してユーザー情報を取得しています。こんな感じのコード:

async getUser(token: string): Promise<User> {
  const { data: { user }, error } = await this.client.auth.getUser(token);

  if (error || !user) {
    throw new UnauthorizedError("Invalid token");
  }

  return {
    id: user.id,
    email: user.email,
    name: user.user_metadata?.full_name,
  };
}

一見普通のコードに見えるんですが、getUser毎回Supabaseのサーバーにリクエストを送る仕様になっていました。

つまり、APIリクエストのたびに:

  1. クライアント → 自分のAPI
  2. 自分のAPI → Supabase Auth API(ここで80ms+)
  3. Supabase Auth API → 自分のAPI
  4. 自分のAPI → クライアント

という往復が発生していた。これは遅いわけです。

getClaims という選択肢

調べてみると、この問題はSupabaseのGitHubでも議論されていました。

そして supabase-js には getClaims というメソッドがあることがわかりました。

const { data, error } = await supabase.auth.getClaims(token);

getUser との違いは:

メソッド動作速度
getUser毎回Auth APIにリクエスト遅い(80ms以上)
getClaimsJWTをローカルで検証速い(数ms)

getClaims は JWTの署名をローカルで検証して、ペイロードに含まれるクレーム(ユーザーIDやメールアドレス)を返してくれます。Supabaseが非対称鍵(RSA/EC)で署名している場合、公開鍵をキャッシュしておけばネットワークリクエストなしで検証できる。

実装を変更してみた

変更はシンプルでした:

async getUser(token: string): Promise<User> {
  const { data, error } = await this.client.auth.getClaims(token);

  if (error || !data) {
    throw new UnauthorizedError("Invalid token");
  }

  const { claims } = data;

  return {
    id: claims.sub,          // user.id → claims.sub
    email: claims.email,     // user.email → claims.email
    name: claims.user_metadata?.full_name,
  };
}

getUser が返す user オブジェクトと、getClaims が返す claims オブジェクトでは、プロパティ名が少し違います:

  • user.idclaims.sub(JWT標準のsubjectクレーム)
  • user.emailclaims.email
  • user.user_metadataclaims.user_metadata

結果

デプロイしてSentryで確認したところ、認証処理が 80ms以上 → 2ms以下 になりました。

変更後:getClaims が 2ms以下に改善

約40倍の高速化。めっちゃ嬉しい。

他のDBクエリが5〜10msくらいなので、認証だけがボトルネックになっていたことがよくわかります。全体のレスポンスタイムもだいぶ改善されました。

注意点

getClaims を使う場合、いくつか気をつけることがあります。

トークンの失効が即座に反映されない

getUser はサーバーに問い合わせるので、ユーザーがログアウトしたり、トークンが無効化されたりした場合に即座に検知できます。

一方 getClaims はローカル検証なので、トークンの有効期限が切れるまでは有効と判断されます。これはJWT認証の宿命ですが、セキュリティ要件が厳しい場合は注意が必要です。

対称鍵の場合はサーバーリクエストが発生する

Supabaseが対称鍵(HS256)で署名している場合、getClaims でもサーバーに問い合わせが発生するようです。非対称鍵(RS256/ES256)を使っているプロジェクトでないとフルの恩恵は得られないかもしれません。

新しいプロジェクトはデフォルトで非対称鍵になっているらしいので、そこは安心。

感想

認証処理は全リクエストで通る部分なので、ここが遅いと全体に影響が出ます。

今回の変更は1ファイルの修正だけで済んだので、同じような状況の人は試してみる価値があると思います。Sentryなどでトレースを見て「認証だけ妙に遅いな」と感じたら、getClaims への切り替えを検討してみてください。

あと、今回改めてSentryでトレースをちゃんと取っておく重要性を感じました。トレースを取っていなかったら、この問題に気づけなかった可能性が高いです。