## 概要 [[Zod]]を規模の大きいコードベースで使うと、開発時のパフォーマンスに大きな影響を及ぼすことが分かった。具体的にはオートコンプリートの速度、ビルド・型チェックの速度など。 そこで、実際に[[GitHub REST API]]の[[OpenAPI Specification]]ファイルを使って、どれくらいパフォーマンスに影響があるかを確かめてみる。具体的には以下のケースを比較してみる。 | ケース | SVL | スキーマ | クライアント | | ------------------------------------ | --------- | -------- | ------------ | | [[#OpenAPI fetch (ケース1)]] | | 自動生成 | 自動生成 | | [[#openapi-zod-client (ケース2)]] | [[Zod]] | 自動生成 | 自動生成 | | [[#OpenAPI Typescript (ケース3)]] | | 自動生成 | 自動生成 | | [[#orval (ケース4)]] | | 自動生成 | 自動生成 | | [[#OpenAPI fetch + Typia (ケース5)]] | [[Typia]] | 自動生成 | 自動生成 | > [!note] > `SVL` は [[スキーマバリデーションライブラリ]] の略。 ## 個人的な結論 あくまで **個人的なもの** であるが、中規模以上のプロダクトで使うなら、1つ目のケースである『[[OpenAPI fetch]]を使用して、[[スキーマバリデーションライブラリ]]は使わない』 がベストだと思った。 [[スキーマバリデーションライブラリ]]がどうしても必要な場合は、スキーマだけを[[OpenAPI Specification]]から自動生成しつつ、クライアントは要件にあわせて自作した方がいいと思う。既存クライアントはよくできているものの、エッジケースでバグが発生したときに対応が求められる。 [[Zodios]]を利用している[[openapi-zod-client]]は、インターフェースもシンプルで、利用方法もカンタン、堅牢なコーディングをしたい場合にも必要な機能が揃っていて一瞬パーフェクトに見えるが、APIの規模がある程度以上の場合、パフォーマンスに深刻な問題が発生する。[[GitHub]] issuesを見る限り、今のところ解消される見込みはなさそう ## 準備 [[GitHub REST API]]の[[OpenAPI Specification]]ファイルを用意する。以下のリポジトリで管理されている。 <div class="link-card"> <div class="link-card-header"> <img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/> <span class="link-card-site-name">GitHub</span> </div> <div class="link-card-body"> <div class="link-card-content"> <p class="link-card-title">GitHub - github/rest-api-description: An OpenAPI description for GitHub's REST API</p> <p class="link-card-description">An OpenAPI description for GitHub's REST API. Contribute to github/rest-api-descriptio ... </p> </div> <img src="https://opengraph.githubassets.com/a1b1b741c1ff53233a2988df6f31cb7810756f4ed4deff1b5ad9c2444f0cbc55/github/rest-api-description" class="link-card-image" /> </div> <a href="https://github.com/github/rest-api-description/tree/main"></a> </div> 今回は[[YAML]]ファイルを使う。7.7MB。21万7000行の大きなファイル。 https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml ## OpenAPI fetch (ケース1) ケース1は[[Zod]]を使わずに、スキーマとクライアントを自動生成するケース。[[OpenAPI fetch]]を使って実現する。 ### インストール ```console npm i openapi-fetch npm i -D openapi-typescript typescript ``` バージョン。 ```console ├── @fsouza/[email protected] ├── @tsconfig/[email protected] ├── [email protected] ├── [email protected] ├── [email protected] └── [email protected] ``` ### スキーマコードの生成 ```console wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml npx openapi-typescript api.github.com.yaml -o api.ts ``` `api.ts`は4.2MB、10万行とかなり大きなものに。 ### コードを書く `index.ts` ```ts import createClient from "openapi-fetch"; import type { paths } from "./api.ts"; async function main() { const client = createClient<paths>({ baseUrl: "https://api.github.com" }); const { data, error } = await client.GET("/search/repositories", { params: { query: { per_page: 3, q: "obsidian" } }, }); if (error) { console.error(error.message); return; } const repositoryNames = data?.items.map((x) => x.name); console.log(repositoryNames); } main(); ``` ### 結果 #### 補完速度 サクサクだったため測定はしていない。 #### 型チェック速度 2秒くらい。0.3秒くらいは`npx tsc`コマンド自体のオーバーヘッドだが、本稿では差分で比較するため気にしない。 ```console ❯ time npx tsc --noEmit npx tsc --noEmit 1.94s user 0.19s system 212% cpu 1.001 total ``` ## openapi-zod-client (ケース2) ケース2は[[Zod]]を利用し、スキーマとクライアントを自動生成するケース。[[openapi-zod-client]]を利用する。 ### インストール ```console npm i -D openapi-zod-client ``` バージョン。 ```console ├── @fsouza/[email protected] ├── @tsconfig/[email protected] ├── [email protected] ├── [email protected] └── [email protected] ``` ### クライアントコードの生成 ```console wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml npx openapi-zod-client api.github.com.yaml -o client.ts ``` `client.ts`は1.4MBで45620行。[[#ケース1]]よりもサイズは小さく1/4程度だが、[[Zodios]]に入力するためのスキーマが定義されているだけだからと思われる。 ### コードを書く `index.ts` ```ts import { createApiClient } from "./client"; async function main() { const client = createApiClient("https://api.github.com"); } main(); ``` 諸事情でここまでしか書けなかった。詳細は[[#ケース2#結果#補完速度]]を参照。 ### 結果 #### 補完速度 遅すぎて測定不能。 この時点で `get` の候補が表示されず[[LSP]]がタイムアウトしてしまった。。 ![[Pasted image 20240511232830.png]] #### 型チェック速度 20秒弱。実質まだ何もしていないコードにしては非常に遅い。また、大量に型エラーが出てしまった。 ```console ❯ time npx tsc --noEmit npx tsc --noEmit 17.57s user 1.15s system 140% cpu 13.354 total ``` ## OpenAPI Typescript (ケース3) ケース3は[[OpenAPI Typescript]]を使用する。 ### インストール ```console npm i -D @hey-api/openapi-ts ``` バージョン。 ```console ├── @fsouza/[email protected] ├── @hey-api/[email protected] ├── @tsconfig/[email protected] ├── [email protected] └── [email protected] ``` ### クライアントコードの生成 ```console wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml npx openapi-ts -i api.github.com.yaml -o client ``` 生成物はファイルではなくディレクトリになる。ファイルサイズは総計で10MBちょい。 ### コードを書く `index.ts` ```ts import { SearchService } from "./client/services.gen"; async function main() { const res = await SearchService.searchRepos({ perPage: 3, q: "Obsidian" }); const repositoryNames = res.items.map((x) => x.name); console.log(repositoryNames); } main(); ``` ### 結果 #### 補完速度 サクサクだったため測定はしていない。 #### 型チェック速度 4秒くらい。ただ、100近くのエラーが出たので動かなかった。 ```console ❯ time npx tsc --noEmit npx tsc --noEmit 3.75s user 0.26s system 191% cpu 2.098 total ``` ## orval (ケース4) ケース4は[[#ケース2]]と同様のことを[[orval]]で行う。 > [!caution] > [[orval]]には[clientに`zod`を指定する機能がある](https://orval.dev/guides/zod) はず... なのだが、実際は以下のエラーになってしまう。。 > > ```error > 🛑 petstore - TypeError: Cannot read properties of undefined (reading 'map') > at parseProperty (/home/tadashi-aikawa/tmp/orval-sandbox/node_modules/@orval/zod/dist/index.js:265:37) > at Array.map > ``` ### インストール ```console npm i -D orval npm i axios ``` バージョン。 ```console ├── @fsouza/[email protected] ├── @tsconfig/[email protected] ├── [email protected] ├── [email protected] └── [email protected] ``` ### クライアントコードの生成 ```console wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml npx orval --input api.github.com.yaml --output client.ts --client zod ``` ### コードを書く `index.ts` ```ts import axios from "axios"; import { searchRepos } from "./client"; axios.defaults.baseURL = "https://api.github.com"; async function main() { const { data } = await searchRepos({ per_page: 3, q: "Obsidian" }); const repositoryNames = data.items.map((x) => x.name); console.log(repositoryNames); } main(); ``` ### 結果 #### 補完速度 サクサクだったため測定はしていない。 #### 型チェック速度 速度は5秒弱。 ```console ❯ time npx tsc --noEmit npx tsc --noEmit 3.50s user 0.31s system 191% cpu 1.986 total ``` [[TS2411]]のエラーが発生するが、無視すれば一応動く。 ## OpenAPI fetch + Typia (ケース5) [[#OpenAPI fetch (ケース1)]]に対して、[[Typia]]で[[スキーマバリデーション]]の機能を追加したもの。 ### インストール ```console npm i openapi-fetch typia npm i -D openapi-typescript typescript ``` バージョン。 ```console ├── @fsouza/[email protected] ├── @tsconfig/[email protected] ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] └── [email protected] ``` ### セットアップ > [!caution] > > `tsconfig.json` に `compilerOptions` プロパティが存在しない場合は、空で設定しておく。そうしないとsetupでエラーになる。 ```console npx typia setup ``` ### スキーマコードの生成 ```console wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml npx openapi-typescript api.github.com.yaml -o api.ts ``` ### コードを書く [[#OpenAPI fetch (ケース1)]]のコードをベースに、[[Typia]]のバリデーションを追加する。 `index.ts` ```ts import createClient from "openapi-fetch"; import typia from "typia"; import type { paths } from "./api.ts"; async function searchRepositories( client: ReturnType<typeof createClient<paths>>, query: { per_page: number; q: string }, ) { return await client.GET("/search/repositories", { params: { query }, }); } async function main() { const client = createClient<paths>({ baseUrl: "https://api.github.com" }); const { data, error } = await searchRepositories(client, { per_page: 3, q: "obsidian", }); if (error) { console.error(error.message); return; } type R = Awaited<ReturnType<typeof searchRepositories>>; const r = typia.validate<R>(data); if (!r.success) { console.error(r.errors); return; } const repositoryNames = r.data.data?.items.map((x) => x.name); console.log(repositoryNames); } main(); ``` この時点でかなり嫌な予感はしている...。 ```console $ npx tsc && node . [ { path: '$input', expected: '(__type.o8 | __type)', value: { total_count: 21120, incomplete_results: false, items: [Array] } } ] ``` 動いているっぽいけど、実質意味をなしてなさそうなvalidate結果になってしまった。使い方が悪いのか、用途がオーバーキルなのかは不明。いずれにせよ、**気軽に使えそうではないので、ハマりたくないのであれば今は使わない方がいい** 気がした。 なお、補完速度などは期待通り問題なかった。validationの実行速度も、`index.js`のコードを見る限り、必要不可欠な処理が実行されているだけのように見える。