Prismaの型処理を100倍ほど改善できるかもしれない知識

に公開
2

結果 (を先にお見せしておきます)

一例としてPrismaのExtendの仕方に気をつけないと、プロジェクトの規模によっては以下と近いことになる可能性もあるのかなと思います。ご興味がある方は続きをお読みくださいませ 🙏
(追記: Extendに限らずよりカジュアルに起こりそうなので別途資料をまとめ中です)

アプローチ Types Instantiations Time Type数改善率 時間短縮率
Heavy 269,668 2,773,122 1.84s - (基準) - (基準)
Interface 3,004 19,098 0.45s 96%削減 75%短縮
Typeof 648 972 0.43s 99.8%削減 77%短縮
Simple 644 972 0.41s 99.8%削減 78%短縮

はじめに

もしかしたら多くの開発者が見落としているかもしれないPrismaのちょっとした使い方が
TypeScriptコンパイラやIDE等の開発環境に思わぬ悪影響を与えることに気付いたので、
その解決策と実際のプロジェクトでの適用例を紹介します。

問題の発見

手前味噌ですが、冒頭に記載した自作のOSSを使ってTypeScriptのパフォーマンスを測定していたところ、あるプロジェクトでPrisma関連のコードに起因してやたらIDEが重くなることに気づきました。
そこで tsc --noEmit --diagnostics などのデバッグコマンドを使って調べてみると、面白いことが分かったのでそのエッセンスを検証データとともに以下にまとました。

検証環境の構築

パフォーマンスの問題を明確に示すため、30段階のネストしたPrismaスキーマを作成しました。
Tree1からTree30まで、各モデルが次のモデルを参照する形で深い階層構造を持つスキーマです。
TypeScriptが大量の型推論を行う、大規模プロジェクトを模した検証環境です。

model Tree1 {
  id        Int      @id @default(autoincrement())
  // ... 基本フィールド
  Tree2     Tree2[]
}

model Tree2 {
  id        Int      @id @default(autoincrement())
  // ... 基本フィールド
  childTree Tree1    @relation(fields: [childId], references: [id])
  Tree3     Tree3?   @relation(fields: [tree3Id], references: [id])
}

// Tree3からTree30まで同様の構造...

実測データによる4つのアプローチの比較

1. Heavy - 直接的なPrismaClient使用

import { PrismaClient } from "@ts-bench/prisma-base";

const extendPrisma = (PrismaClient: PrismaClient) => {
  console.log("Extend PrismaClient with some logger and other features...");
  return PrismaClient;
};

const client = new PrismaClient({ datasourceUrl: "file:./sample.db" });
const extendedClient = extendPrisma(client);

実測結果:

  • Types: 269,668
  • Instantiations: 2,773,122
  • Total time: 1.84s

2. Interface - 最小限のインターフェースアプローチ

interface IPrismaTreeClient {
  tree1: PrismaClient["tree1"];
}

const extendPrisma = <T extends IPrismaTreeClient>(PrismaClient: T): T => {
  console.log("Extend PrismaClient with some logger and other features...");
  return PrismaClient;
};

実測結果:

  • Types: 3,004 (96%削減)
  • Instantiations: 19,098 (99.3%削減)
  • Total time: 0.45s (75%短縮)

3. Typeof - typeof演算子を活用したアプローチ

const extendPrisma = (PrismaClient: typeof client) => {
  console.log("Extend PrismaClient with some logger and other features...");
  return PrismaClient;
};

const client = new PrismaClient({ datasourceUrl: "file:./sample.db" });
const extendedClient = extendPrisma(client);

実測結果:

  • Types: 648 (99.8%削減)
  • Instantiations: 972 (99.96%削減)
  • Total time: 0.43s (77%短縮)

4. Simple - 拡張なしのベースライン

const client = new PrismaClient({ datasourceUrl: "file:./sample.db" });
const result = await client.tree1.findMany();

実測結果:

  • Types: 644 (99.8%削減)
  • Instantiations: 972 (99.96%削減)
  • Total time: 0.41s (78%短縮)

パフォーマンス改善効果の分析

検証結果を表にまとめると、その差は歴然です:

アプローチ Types Instantiations Time Type数改善率 時間短縮率
Heavy 269,668 2,773,122 1.84s - (基準) - (基準)
Interface 3,004 19,098 0.45s 96%削減 75%短縮
Typeof 648 972 0.43s 99.8%削減 77%短縮
Simple 644 972 0.41s 99.8%削減 78%短縮

最も驚くべきは、Heavy アプローチが 2,773,122 のインスタンス化を引き起こすのに対し、
TypeofSimple アプローチでは 972 まで削減される点です。
これは 99.96% という劇的な改善率を示しています。

なぜこれほど差が生まれるのか?

TypeScriptコンパイラは、型の参照時に必要な型情報をすべて解析しようとします。
以下それぞれのアプローチの特徴をAI様のお力をかりつつご紹介します。
(言われてみればそうだなとなりますが、普段から注意し続けるのは難しい部分もありますね 🙄)

Heavy アプローチの問題点

  1. PrismaClient全体の型情報が関数パラメータとして要求される

    • TypeScriptが全てのリレーション(Tree1→Tree2→...→Tree30)を追跡しようとする
    • 各段階で指数関数的に型の複雑さが増大する
    • 30層のネストにより、型チェッカーが膨大な組み合わせを計算する
  2. 型の展開が再帰的に発生

    • PrismaClientの型定義には、全モデルの関係性が含まれている
    • 関数に渡す際、これらすべての型情報を解決しようとする
    • 結果として270万を超えるインスタンス化が発生

Interface・Typeof アプローチの効果

  1. 型クエリによる効率的な型参照

    • typeofは既存の識別子式から型を取得し、複雑な型定義の再展開を回避
    • 型の範囲が限定されるため、不要な型推論が発生しない
    • TypeScriptが処理する型の複雑度を大幅に削減
  2. 型インスタンス化の最適化

    • 型クエリにより、実際に使用される型のみが参照される
    • インターフェースにより型の境界を明確にし、余分な型解析を回避
    • 結果として99.96%のインスタンス化削減を実現

この違いは、大規模なPrismaスキーマを持つ実際のプロジェクトでも同様に現れ、開発体験に直接的な影響を与えます。

実プロジェクトでの適用例

実際のプロジェクトでこの問題に遭遇した場合の対処法:

❌ 避けるべきパターン

const extendPrisma = (prisma: PrismaClient) => {
  // 大量の型推論が発生
  return prisma.$extends({
    // 拡張処理
  });
};

✅ 推奨パターン 1: Interface活用

export interface IPrismaClient {
  $extends: PrismaClient['$extends']
}

const extendPrisma = <T extends IPrismaClient>(prisma: T): T => {
  return prisma.$extends({
    // 拡張処理
  });
};

✅ 推奨パターン 2: Typeof活用

const basePrisma = new PrismaClient();

const extendPrisma = (prisma: typeof basePrisma) => {
  return prisma.$extends({
    // 拡張処理
  });
};

開発環境への影響

この最適化により以下の改善が期待できます:

  • IDE の応答速度向上: VSCodeなどでの型チェックが高速化
  • ホットリロード時間短縮: 開発中の再コンパイル時間が大幅に削減
  • CI/CD パイプライン高速化: 型チェック処理時間の短縮
  • 開発者体験の向上: 型エラーの表示やオートコンプリートが快適に

特に大規模なPrismaスキーマを持つプロジェクトでは、この違いが開発効率に直結する重要な要素となります。

まとめ

Prismaを使用したプロジェクトにおいて、型の扱い方一つでTypeScript関連の指標が大きく変わることを実測データで確認しました。

日々の開発でこうした細かい点に気を配ることで、より快適な開発体験を実現できます。
また、冒頭で紹介したようなパフォーマンス監視ツールを導入することで、このような問題を早期に発見し、対処することが可能になります。

おしらせ

今回のような開発環境にあたえるインパクトの測定、改善に役立つつOSS TS-Bench: GitHub を作っています。AIがパフォーマンス劣化を警告したり対策案を提案してくれるのでご興味ある方はぜひGitHubからスターをお願いします 🙇‍♂️
(まだ安定して利用できる品質では無いので、今後のクオリティアップにご期待ください!)

免責事項

  • 本検証は特定環境での結果であり、実際の適用時は各環境での統計的な検証を推奨します
    (実際の検証内容は https://github.com/ToyB0x/ts-bench/pull/190 を参照してください)
  • 本記事はLLMと手書きを併用して作成しました

Discussion

derodero24derodero24

うちもPrismaの型推論には困っていたので参考になります。
Prisma.defineExtensionとの違いも気になるところですね。

toyb0xtoyb0x

コメント頂きありがとうございます!
DBスキーマが小さいうちは気付きにくい部分なので、なかなか難しい所ですよね!

また、コメント頂いたdefineExtensionとextendsを組み合わるPrismaの実装例などもあったりして、
ぱっと見で大丈夫かは分かりにく所だな〜とも思います 🙄

そういった分かりにくい部分でうっかりよろしくないコードを書いてしまった時に
CIで手軽に気づけるようにしたいというのがTS-Bench: GitHubを作っている理由でもあります。

おそらくあと1ヶ月ほどである程度使えるものになると思いますので、
安定版が出たらぜひお試しくださいませ 🙏