Slide 1

Slide 1 text

16年目のピクシブ百科事典 を支える最新の技術基盤 - 自然物から人工物へ - pixiv Inc. ahu 2026-01-13

Slide 2

Slide 2 text

2 自己紹介 ● 2021年4月 新卒入社 ● フロントエンドエンジニア ● pixiv本体の、マンガビューア・小説文字数 チェッカー・ストリートの実装などを担当 ● 2024年6月から百科事典チームに参加 ● 技術面のリーダーとして、機能開発や基盤整備 の計画・進行・実装・レビューなどを担当 ahu エンジニア

Slide 3

Slide 3 text

今日話すこと ● 意思が反映しにくいコードでは何が起きるか、今あるものにどう向き合うか ● 具体的なツールの活用事例やしていること、チームでの取り組みなどの紹介 3

Slide 4

Slide 4 text

今日話すこと ● 意思が反映しにくいコードでは何が起きるか、今あるものにどう向き合うか ● 具体的なツールの活用事例やしていること、チームでの取り組みなどの紹介 話さないこと ● Next.js の機能の深掘りや実装テクニック、ベストプラクティスについて ● Next.js のパフォーマンスの問題を具体的にどう解決したか ⇒ こちらは社内ブログで細かく書いてます!ぜひそちらで! 4

Slide 5

Slide 5 text

5 ピクシブ百科事典 というサービス https://dic.pixiv.net/

Slide 6

Slide 6 text

6 ● 「みんなでつくる百科事典」が合言 葉の、オンライン百科事典サービス ● 2009年11月10日にβ版をリリース ○ 今年で16年目に突入!🎉 ピクシブ百科事典 というサービス https://dic.pixiv.net/

Slide 7

Slide 7 text

7 ● 「みんなでつくる百科事典」が合言 葉の、オンライン百科事典サービス ● 2009年11月10日にβ版をリリース ○ 今年で16年目に突入!🎉 ● バックエンドは PHP 8.5 + k8s ● フロントエンドは Next.js 15 ● Smarty(テンプレートエンジン) → Next.js へ移行中…… ピクシブ百科事典 というサービス https://dic.pixiv.net/

Slide 8

Slide 8 text

16年目のサービスが Next.js に!? 8

Slide 9

Slide 9 text

簡単ではなかった Next.js 化 😭 ● 移行を始めたのは2023年の2月頃(自分が参加する1年半前) ● コロナ禍によるアクセス数の増加・テンプレートエンジンによる実装の古さ & メンテナンスの難しさなどがあり、Next.js へのリプレイスが決まる 9

Slide 10

Slide 10 text

簡単ではなかった Next.js 化 😭 ● 移行を始めたのは2023年の2月頃(自分が参加する1年半前) ● コロナ禍によるアクセス数の増加・テンプレートエンジンによる実装の古さ & メンテナンスの難しさなどがあり、Next.js へのリプレイスが決まる ● 当時のチーム構成はバックエンド1名・フロントエンド1名(兼務)という最小構成 ● 主要なページの Next.js 化は実現できたが、表示まで最大で10秒近くかかる場合が あるなど、パフォーマンス面で重い課題が残る状態(負荷で 503 エラーも頻発) ● これを改善しないと全面的なリリースは難しい……という状況に 10

Slide 11

Slide 11 text

11 ● SSR の負荷を下げ表示速度を大幅 に短縮 ⇒ リリースはできた 🎉 ● しかし…… パフォーマンスは 改善できたが…… https://inside.pixiv.blog/2025/11/26/190000 負荷が下がり 503 エラーも激減

Slide 12

Slide 12 text

12 ● SSR の負荷を下げ表示速度を大幅 に短縮 ⇒ リリースはできた 🎉 ● しかし……パフォーマンス面の制御 が難航した = チームの意思を反映 しにくいコードである ということ ● 例えば、ディレクトリ構成や実装 パターンが多様で把握が困難…… という課題は残ったまま パフォーマンスは 改善できたが…… https://inside.pixiv.blog/2025/11/26/190000 負荷が下がり 503 エラーも激減

Slide 13

Slide 13 text

意思が反映しにくいコードを 一度の改善でどうにかするのは難しい 意思 = PdM やエンジニアやチームがやりたいこと 意思が反映しやすコードにするには、継続的に向き合う必要がある 13

Slide 14

Slide 14 text

14 ● Q. 1日に何回デプロイしますか? ● 日々書き換わる中で、コードは自然 と多様で無秩序な方向に育っていく = エントロピー増大の法則 ● 複雑度が勝手に下がることはない どうして一度の改善 では難しいのか

Slide 15

Slide 15 text

15 ● Q. 1日に何回デプロイしますか? ● 日々書き換わる中で、コードは自然 と多様で無秩序な方向に育っていく = エントロピー増大の法則 ● 複雑度が勝手に下がることはない ⇒ コードの成長に方向性を与えたり、 成長の方向を矯正し続ける必要がある どうして一度の改善 では難しいのか

Slide 16

Slide 16 text

コードの成長に方向性を与えたり、 成長の方向を矯正し続ける必要がある 16

Slide 17

Slide 17 text

では実際に何をしているか ここからは具体例を紹介します 17

Slide 18

Slide 18 text

では実際に何をしているか データフローの改善 18

Slide 19

Slide 19 text

1. API の仕様から TS の型定義を生成する ● 元々バックエンドでは、ドキュメンテーションのために Swagger が運用されていた ● しかしフロントエンドではその定義を使っておらず、API に渡す値の型や レスポンスの型定義について、正確さを人が担保する運用だった ● 特にレスポンスの型定義が不正確で、リファクタリングで不要な処理が増えたり、 必要な処理を消してしまったりなど、他の全ての改善を妨げていた 19

Slide 20

Slide 20 text

1. API の仕様から TS の型定義を生成する ● 元々バックエンドでは、ドキュメンテーションのために Swagger が運用されていた ● しかしフロントエンドではその定義を使っておらず、API に渡す値の型や レスポンスの型定義について、正確さを人が担保する運用だった ● 特にレスポンスの型定義が不正確で、リファクタリングで不要な処理が増えたり、 必要な処理を消してしまったりなど、他の全ての改善を妨げていた ● 現在は openapi-typescript で swagger.json から TypeScript の型を生成 ● パスやパラメーターに間違いがないか型チェック & 補完が効くように ✅ 20

Slide 21

Slide 21 text

// before: パスやパラメータを手で指定しており、typo や値の変換漏れに気付けない // before: クエリパラメータの指定順を間違えると、SSR 時の fallback を有効に使えない // before: 戻り値の型も、パスやパラメータからは引けず手で指定する必要がある export function useComments(tag: string, limit: number) { const { locale } = useLocale(); const encodedTagName = encodeTagName(tag); const { data, error } = useSWR( `/get_comments/${encodedTagName}?lang=${locale}&limit=${limit}`, ); return useDataOrError(data, error); } // after: パスやパラメータの補完が効き、typo や値の変換漏れを型エラーとして検出 // after: SSR 時もこの記法が使え、クエリパラメータの指定順を気にする必要がない // after: 戻り値の型も、パスやパラメータから推論され手で指定する必要がない // after: ついでに、2重エンコード防止のため URL エンコード済みの値を幽霊型で区別 export function useComments(tag: TagName, limit: number) { const { locale } = useLocale(); const { data, error } = useQuery("/get_comments/{tagName}", { params: { path: { tagName: tag }, query: { lang: locale, limit } }, }) return useDataOrError(data, error); } 21

Slide 22

Slide 22 text

2. ディレクトリ構成の再定義 ● 全てのコンポーネントが src/components にあり、文脈が掴みづらい状態だった ● 責任の境界が曖昧になり、それぞれのコンポーネントの役割が拡大しがちに 22

Slide 23

Slide 23 text

2. ディレクトリ構成の再定義 ● 全てのコンポーネントが src/components にあり、文脈が掴みづらい状態だった ● 責任の境界が曖昧になり、それぞれのコンポーネントの役割が拡大しがちに ● Bulletproof React という設計を参考に、features という関心事(ページであった り機能であったり)ごとにディレクトリを区切るよう、全体の構成を見直し ● pages 以下を、表示のルーティングとコンポーネントの配置だけをする薄い コントローラーとして扱うことで、Next.js 固有の問題の特定が容易になる効果も 23

Slide 24

Slide 24 text

24 src ├── pages # Next.js が特別扱いする場所で、ページ構成を定義する │ ├── a/[tagName] │ │ ├── index.page.tsx # データの取得と、 DesktopView と MobileView の出し分けを担当 │ │ ├── DesktopView.tsx # features に実装したコンポーネントを配置して画面を作る │ │ └── MobileView.tsx │ └── ... ├── features # ページや機能など、個別の関心事ごとにコードを置く │ ├── article-common │ ├── article # 記事ページに必要な article-common 以外の部分の実装。再帰的な構造 │ │ ├── components │ │ ├── hooks │ │ ├── utils.ts │ │ └── types.ts │ ├── history # 履歴ページに必要な article-common 以外の部分の実装 │ └── ... ├── lib # 広告や翻訳データなど、外部リソースに関するコードを置く ├── components # 全体で使う汎用的なコンポーネントを置く ├── hooks # React に依存した汎用的な補助関数(フック)を置く │ ├── api │ └── ... ├── utils # React に無関係な、その他の汎用的な補助関数を置く │ ├── ssr # 特に SSR に関する補助関数を置く │ └── … └── types

Slide 25

Slide 25 text

25 src ├── pages # Next.js が特別扱いする場所で、ページ構成を定義する │ ├── a/[tagName] │ │ ├── index.page.tsx # データの取得と、 DesktopView と MobileView の出し分けを担当 │ │ ├── DesktopView.tsx # features に実装したコンポーネントを配置して画面を作る │ │ └── MobileView.tsx │ └── ... ├── features # ページや機能など、個別の関心事ごとにコードを置く │ ├── article-common │ ├── article # 記事ページに必要な article-common 以外の部分の実装。 再帰的な構造 │ │ ├── components │ │ ├── hooks │ │ ├── utils.ts │ │ └── types.ts │ ├── history # 履歴ページに必要な article-common 以外の部分の実装 │ └── ... ├── lib # 広告や翻訳データなど、外部リソースに関するコードを置く ├── components # 全体で使う汎用的なコンポーネントを置く ├── hooks # React に依存した汎用的な補助関数(フック)を置く │ ├── api │ └── ... ├── utils # React に無関係な、その他の汎用的な補助関数を置く │ ├── ssr # 特に SSR に関する補助関数を置く │ └── … └── types

Slide 26

Slide 26 text

では実際に何をしているか 変化の監視や基盤の整理 26

Slide 27

Slide 27 text

27 ● Storybook やスナップショットテ ストの対象は、コンポーネント単体 ● コンポーネント同士を組み合わせた 際の差分も、視覚的に確認したい! ● Playwright と reg-cli でビジュア ルリグレッションテストを導入 ● CI で実行して PR にコメント 3. VRT の導入と レポートの通知 実際に差分が出ている様子(赤い部分が差分)

Slide 28

Slide 28 text

28 ● デプロイや CI の設定は、一度作る とあまり更新されず属人化しがち ● でも毎日使うものでもある ● 設定と Docker イメージのビルドを 見直し、CI の待ち時間を1/4に 🎉 ● コメントで丁寧めに意図も補足 ● 最近は standalone ビルドに移行 4. デプロイ周りの 整理と CI の高速化 全体で10分(それぞれ 3分くらい)

Slide 29

Slide 29 text

29 ● 使わなかったコードは消し忘れがち ● Knip というツールで、未使用の export や宣言、package.json の 不要な記述や記載漏れをチェック ● 不要なコンテキストを減らせば、 AI によるコード補完の精度向上 なども期待できるはず 5. 不要な実装と 依存関係の整理 https://knip.dev/

Slide 30

Slide 30 text

30 ● classnames というスタイル関連の ライブラリを、後発でより軽量な clsx/lite に置き換えた ● 周辺ツールやエディタの恩恵をより 多く受けられるよう、ts-morph などで codemod を作ることも ● codemod をレビューすれば、 レビュー量がずっと少なくなる 6. ライブラリの 更新と移行作業

Slide 31

Slide 31 text

百科事典では何をしているか チームとレビューの体制 31

Slide 32

Slide 32 text

32 ● 「こうあってほしい」を起票 → 提案や議論の過程も追記していく ● 低コストで技術選定の理由を参照で きるドキュメント(ADR)が完成 ● 設計の共通言語を作るため、The Elm Architecture や リーダブル コードなどの読書会を企画 7. 低コストな ADR の運用 & 読書会

Slide 33

Slide 33 text

33 ● ピクシブでは、Gemini を使った レビュー bot が運用されている ● プロンプトとして、暗黙的だった レビュー基準を整理 & 言語化 ● 単純な実装ミスのサジェストに利用 ● セルフレビューが捗り、より本質的 な議論に時間を割けるように 8. 基準の整備と bot によるレビュー

Slide 34

Slide 34 text

まとめ ● 意思が反映しにくいコードだと、リリースの可否に影響が出ることも…… ● コードは自然と多様で無秩序な方向に育っていく(エントロピー増大の法則) ● コードの成長に方向性を与えたり、成長の方向を矯正し続ける必要がある ● サブタイトルや元の記事で「自然物から人工物へ」と書いてるのもこの意味 ● 具体的なツールとしては、OpenAPI TypeScript や Knip や Gemini などを活用 ● ADR を残しやすくする仕組みや読書会など、チーム体制への投資も重要 34