diff --git a/Dockerfile b/Dockerfile index 2c95811428..a25e0148b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,34 @@ # syntax=docker/dockerfile:1 -ARG NODE_VERSION=24.14.0 -ARG PNPM_VERSION=10.32.1 +ARG BUN_VERSION=1.3.9 +# Linux amd64 向け(Fly.io 実行環境・Apple Silicon 上の docker build とのズレ防止) +ARG TARGETPLATFORM=linux/amd64 -FROM node:${NODE_VERSION}-slim AS base +FROM --platform=$TARGETPLATFORM oven/bun:${BUN_VERSION} AS base LABEL fly_launch_runtime="Node.js" -ENV PNPM_HOME=/pnpm - WORKDIR /app -RUN --mount=type=cache,target=/root/.npm npm install -g pnpm@${PNPM_VERSION} FROM base AS build -COPY ./application/package.json ./application/pnpm-lock.yaml ./application/pnpm-workspace.yaml ./ +COPY ./application/package.json ./application/bun.lock ./ COPY ./application/client/package.json ./client/package.json COPY ./application/server/package.json ./server/package.json -RUN --mount=type=cache,target=/pnpm/store pnpm install --frozen-lockfile +COPY ./application/e2e/package.json ./e2e/package.json +RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile COPY ./application . -RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build +RUN NODE_OPTIONS="--max-old-space-size=4096" bun run build -RUN --mount=type=cache,target=/pnpm/store CI=true pnpm install --frozen-lockfile --prod --filter @web-speed-hackathon-2026/server +RUN --mount=type=cache,target=/root/.bun/install/cache CI=true bun install --frozen-lockfile --production --filter @web-speed-hackathon-2026/server FROM base +RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* + COPY --from=build /app /app EXPOSE 8080 -CMD [ "pnpm", "start" ] +CMD [ "bun", "run", "start" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..7519960e00 --- /dev/null +++ b/Makefile @@ -0,0 +1,189 @@ +# Web Speed Hackathon 2026 — docs/README に沿ったよく使う操作 +# 前提: mise で Node 24 / Bun が使えること(mise.toml・docs/development.md) + +.DEFAULT_GOAL := help + +APP_DIR := application +SCORE_DIR := scoring-tool +# ローカル起動時のデフォルト(application/README.md) +APPLICATION_URL ?= http://localhost:3000 +# E2E テストの絞り込み(空の場合は全件) +E2E_SPEC ?= +E2E_GREP ?= + +.PHONY: help +help: + @echo "利用可能なターゲット(詳細は docs/・各 README 参照):" + @echo "" + @echo " セットアップ" + @echo " make setup … mise trust + mise install + アプリ・計測ツールの bun install" + @echo " make mise-trust … mise trust(初回・mise.toml 変更時)" + @echo " make mise-install … mise install(Node / Bun のバージョン固定)" + @echo " make install-app … $(APP_DIR) で bun install --frozen-lockfile" + @echo " make install-scoring… $(SCORE_DIR) で bun install --frozen-lockfile" + @echo "" + @echo " CaX アプリケーション($(APP_DIR))" + @echo " make build … クライアントビルド(bun run build)" + @echo " make analyze … クライアント bundle 分析(webpack-bundle-analyzer)" + @echo " make start … サーバー起動(bun run start)→ 通常 http://localhost:3000/" + @echo " make typecheck … ワークスペース全体の型チェック" + @echo " make format … oxlint --fix + oxfmt" + @echo " make clean … ビルド成果物・E2E 一時出力の削除($(APP_DIR)/dist など)" + @echo "" + @echo " シード(server パッケージ)" + @echo " make seed-generate … シード生成" + @echo " make seed-insert … DB へシード投入" + @echo " make db-reset … DB を初期シードで再作成" + @echo "" + @echo " E2E / VRT(application/README.md)" + @echo " make playwright-install … Chromium の取得(初回)" + @echo " make e2e-test … Playwright 実行(E2E_SPEC/E2E_GREP で絞り込み可)" + @echo " make e2e-core … ホーム/検索/投稿詳細/利用規約/レスポンシブ" + @echo " make e2e-auth … 認証/ユーザー詳細" + @echo " make e2e-post … 投稿機能中心" + @echo " make e2e-dm … DM 機能" + @echo " make e2e-crok … Crok チャット" + @echo " make e2e-full … 全件実行(最終確認向け)" + @echo " make e2e-update … スナップショット更新(環境差分がある場合)" + @echo " E2E_BASE_URL=https://... make e2e-test … リモート向け" + @echo " make e2e-test E2E_SPEC=\"src/posting.test.ts\" E2E_GREP=\"画像\"" + @echo "" + @echo " ローカル採点(scoring-tool/README.md)" + @echo " make score … Lighthouse 計測(APPLICATION_URL を上書き可)" + @echo " make score-targets … 計測名一覧" + @echo " make format-scoring … scoring-tool のフォーマット" + @echo "" + @echo " コンテナ(Dockerfile — 本番相当ビルド)" + @echo " make docker-build … docker build(ポート 8080 想定)" + +.PHONY: setup mise-trust mise-install install install-app install-scoring +setup: mise-trust mise-install install + +mise-trust: + mise trust + +mise-install: + mise install + +install: install-app install-scoring + +install-app: + cd $(APP_DIR) && bun install --frozen-lockfile + +install-scoring: + cd $(SCORE_DIR) && bun install --frozen-lockfile + +.PHONY: build analyze start typecheck format clean +build: + cd $(APP_DIR) && bun run build + +analyze: + cd $(APP_DIR) && bun run analyze + +clean: + rm -rf $(APP_DIR)/dist $(APP_DIR)/e2e/test-results + +start: + cd $(APP_DIR) && bun run start + +typecheck: + cd $(APP_DIR) && bun run typecheck + +format: + cd $(APP_DIR) && bun run format + +.PHONY: seed-generate seed-insert db-reset +seed-generate: + cd $(APP_DIR) && bun run --filter @web-speed-hackathon-2026/server seed:generate + +seed-insert: + cd $(APP_DIR) && bun run --filter @web-speed-hackathon-2026/server seed:insert + +db-reset: seed-generate + cd $(APP_DIR) && bun run --filter @web-speed-hackathon-2026/server seed:insert + +.PHONY: playwright-install e2e-test e2e-core e2e-auth e2e-post e2e-dm e2e-crok e2e-full e2e-update \ + e2e-home e2e-search e2e-post-detail e2e-terms e2e-responsive \ + e2e-auth-auth-modal e2e-user-profile \ + e2e-posting e2e-dm-single e2e-crok-chat-single +playwright-install: + cd $(APP_DIR)/e2e && bunx playwright install chromium + +e2e-test: + cd $(APP_DIR)/e2e && \ + $(if $(E2E_BASE_URL),E2E_BASE_URL=$(E2E_BASE_URL) ,)bunx playwright test $(E2E_SPEC) $(if $(E2E_GREP),--grep "$(E2E_GREP)",) + +e2e-core: + $(MAKE) e2e-home + $(MAKE) e2e-search + $(MAKE) e2e-post-detail + $(MAKE) e2e-terms + $(MAKE) e2e-responsive + +e2e-auth: + $(MAKE) e2e-auth-auth-modal + $(MAKE) e2e-user-profile + +e2e-post: + $(MAKE) e2e-posting + $(MAKE) e2e-post-detail + +e2e-dm: + $(MAKE) e2e-dm-single + +e2e-crok: + $(MAKE) e2e-crok-chat-single + +# 以下は e2e の細かい分割ターゲット(テストファイル単位) +# +# 既存の大枠ターゲット(e2e-core/e2e-auth/e2e-post/...)は後方互換のため残し、 +# これらの小分けターゲットを呼ぶようにしています。 +e2e-home: + $(MAKE) e2e-test E2E_SPEC="src/home.test.ts" + +e2e-search: + $(MAKE) e2e-test E2E_SPEC="src/search.test.ts" + +e2e-post-detail: + $(MAKE) e2e-test E2E_SPEC="src/post-detail.test.ts" + +e2e-terms: + $(MAKE) e2e-test E2E_SPEC="src/terms.test.ts" + +e2e-responsive: + $(MAKE) e2e-test E2E_SPEC="src/responsive.test.ts" + +e2e-auth-auth-modal: + $(MAKE) e2e-test E2E_SPEC="src/auth.test.ts" + +e2e-user-profile: + $(MAKE) e2e-test E2E_SPEC="src/user-profile.test.ts" + +e2e-posting: + $(MAKE) e2e-test E2E_SPEC="src/posting.test.ts" + +e2e-dm-single: + $(MAKE) e2e-test E2E_SPEC="src/dm.test.ts" + +e2e-crok-chat-single: + $(MAKE) e2e-test E2E_SPEC="src/crok-chat.test.ts" + +e2e-full: + $(MAKE) e2e-test + +e2e-update: + cd $(APP_DIR)/e2e && bunx playwright test --update-snapshots + +.PHONY: score score-targets format-scoring +score: + cd $(SCORE_DIR) && bun run start --applicationUrl $(APPLICATION_URL) + +score-targets: + cd $(SCORE_DIR) && bun run start --applicationUrl $(APPLICATION_URL) --targetName + +format-scoring: + cd $(SCORE_DIR) && bun run format + +.PHONY: docker-build +docker-build: + docker build -t web-speed-hackathon-2026 . diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 0000000000..cdb6645960 --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,690 @@ +# パフォーマンス問題一覧 + +## 対応履歴 + +| # | 問題 | 対応日 | 効果 | コミット | +|---|------|--------|------|--------| +| 1 | Webpack最適化無効 | 2026-03-20 | main.js: 108MB→245KB+73MB(wasm込) / minify+splitChunks有効化 | perf: webpack最適化有効化 | +| 2 | jQuery async:false | 2026-03-20 | メインスレッドブロック解消 / fetch()へ置き換え・jQuery削除 | perf: jQuery→fetchへ置き換え | +| 3 | InfiniteScroll 26万配列 | 2026-03-20 | スクロールGC負荷解消 / IntersectionObserver化 | perf: IntersectionObserver化 | +| 4 | Post defaultScope 全関連ロード | 2026-03-20 | 不要時の関連ロード排除 / scope('withAll')化 | perf: Post/Commentのdefaultscope最適化 | +| 5 | Comment defaultScope 全ユーザーロード | 2026-03-20 | 同上 | 同上 | + +--- + +## 🔴 CRITICAL + +### 1. ✅ Webpack 最適化が全て無効 [対応済 2026-03-20] +**ファイル:** `application/client/webpack.config.js:130-137` + +```javascript +optimization: { + minimize: false, // 無効 + splitChunks: false, // コード分割なし + concatenateModules: false, // 無効 + usedExports: false, // Tree-shaking 無効 + providedExports: false, + sideEffects: false, +} +``` + +**影響:** main.js が **108MB** のまま配信される。適切に最適化すれば 5〜10MB 程度になるはず。 + +--- + +### 2. ✅ jQuery AJAX が全て同期実行 [対応済 2026-03-20] +**ファイル:** `application/client/src/utils/fetchers.ts:5-57` + +```javascript +$.ajax({ + async: false, // JS イベントループを完全ブロック + ... +}) +``` + +全ての画像・動画・API リクエストがメインスレッドをブロックする。`fetch()` に置き換えるだけで解消。 + +--- + +### 3. ✅ InfiniteScroll が毎スクロールで 26 万要素の配列を生成 [対応済 2026-03-20] +**ファイル:** `application/client/src/components/foundation/InfiniteScroll.tsx:14-39` + +```javascript +const hasReached = Array.from(Array(2 ** 18), () => { // 262,144 回! + return window.innerHeight + Math.ceil(window.scrollY) >= document.body.offsetHeight; +}).every(Boolean); +``` + +スクロール・wheel・resize・touchmove のたびに毎秒 10 回以上実行。GC 負荷・ガタつきの原因。`IntersectionObserver` で代替可能。 + +--- + +### 4. ✅ Post モデルの defaultScope が全リレーションを常にロード [対応済 2026-03-20] +**ファイル:** `application/server/src/models/Post.ts:45-66` + +```javascript +defaultScope: { + include: [ + { association: "user", include: [{ association: "profileImage" }] }, + { association: "images", through: { attributes: [] } }, + { association: "movie" }, + { association: "sound" }, + ], +} +``` + +`Post.findAll()` を呼ぶだけで user / profileImage / images / movie / sound が毎回 JOIN される。タイムライン 30 件で 30+ クエリ。 + +--- + +### 5. ✅ Comment モデルも同様に全ユーザー情報を常にロード [対応済 2026-03-20] +**ファイル:** `application/server/src/models/Comment.ts:43-54` + +コメント 50 件 = user + profileImage が 50 件分自動ロード。 + +--- + +### 6. DirectMessageConversation が全メッセージ + 全ユーザー情報をロード +**ファイル:** `application/server/src/models/DirectMessageConversation.ts:49-59` + +```javascript +defaultScope: { + include: [ + { association: "initiator", include: [{ association: "profileImage" }] }, + { association: "member", include: [{ association: "profileImage" }] }, + { + association: "messages", + include: [{ association: "sender", include: [{ association: "profileImage" }] }], + // ← ページネーションなし: 全メッセージ取得 + }, + ], +} +``` + +会話数 × メッセージ数 の O(n*m) クエリ。 + +--- + +### 7. 静的ファイルのキャッシュが完全に無効 +**ファイル:** `application/server/src/app.ts:16-22` + +```javascript +res.header({ + "Cache-Control": "max-age=0, no-transform", // 全レスポンスにキャッシュなし +}); +``` + +**ファイル:** `application/server/src/routes/static.ts:16-27` + +```javascript +serveStatic(UPLOAD_PATH, { etag: false, lastModified: false }) +serveStatic(PUBLIC_PATH, { etag: false, lastModified: false }) +``` + +365MB の画像・動画が毎ページロード時に再ダウンロードされる。 + +--- + +### 8. 検索 API が 2 回フルスキャン + アプリ側でマージ・ソート +**ファイル:** `application/server/src/routes/api/search.ts:41-75` + +1. テキスト検索クエリ (1 回目) +2. ユーザー名検索クエリ (2 回目・ページネーションなし) +3. アプリ側で重複除去 → sort → slice + +UNION SQL 1 本で解決できる。 + +--- + +### 9. CROK (SSE) に人工的な遅延 +**ファイル:** `application/server/src/routes/api/crok.ts:36-46` + +```javascript +await sleep(3000); // 初回レスポンスまで 3 秒 +// ... +await sleep(10); // 1 文字ごとに 10ms +``` + +--- + +### 10. 全画像を ArrayBuffer で取得して EXIF・サイズ解析 +**ファイル:** `application/client/src/components/foundation/CoveredImage.tsx:25-39` + +画像 1 枚ごとに: +1. 全データを ArrayBuffer でフェッチ (最大 6.7MB) +2. `sizeOf()` でサイズ解析 +3. `load()` で EXIF 解析 +4. `URL.createObjectURL()` で Blob 化 + +タイムライン 30 枚 = 30 回この処理が走る。`` に置き換えれば解消。 + +--- + +### 11. initialize 時に SQLite ファイルを丸ごとコピー +**ファイル:** `application/server/src/sequelize.ts:12-28` + +毎回 SQLite ファイル全体を tmpdir にコピーする。DB サイズが大きいほど遅い。 + +--- + +## 🟠 MAJOR + +### 12. バンドルに重い不要ライブラリが含まれる + +| ライブラリ | サイズ | 問題 | +|-----------|--------|------| +| jQuery | ~87KB min+gz | `fetch()` で代替可能 | +| Lodash (フル) | ~70KB min+gz | `import _ from 'lodash'` でフル読込。ネイティブ Array メソッドで代替可能 | +| Moment.js | ~67KB min+gz | `.format("LL")` と `.fromNow()` にしか使っていない。`Intl` API で代替可能 | +| core-js / regenerator-runtime | 大 | entry に無条件追加。現代ブラウザでは不要 | + +--- + +### 13. React.memo が一切なし +**ファイル:** +- `application/client/src/components/timeline/TimelineItem.tsx` +- `application/client/src/components/post/PostItem.tsx` +- `application/client/src/components/post/CommentItem.tsx` + +親が再レンダーすると全投稿・全コメントが再レンダー。 + +--- + +### 14. 画像・動画に lazy loading がない +`loading="lazy"` や `IntersectionObserver` なし。スクロール範囲外のメディアも即時ロード。 + +--- + +### 15. useInfiniteFetch が全件取得後にクライアント側でスライス +**ファイル:** `application/client/src/hooks/use_infinite_fetch.ts:39-43` + +```javascript +fetcher(apiPath).then((allData) => { + setResult((cur) => ({ + data: [...cur.data, ...allData.slice(offset, offset + LIMIT)], // 全件取得後に切り出し + })); +}); +``` + +--- + +### 16. 投稿一覧 API がデフォルトで全件返す +**ファイル:** `application/server/src/routes/api/post.ts:8-14` + +`limit` が指定されない場合、DB の全投稿を返す。 + +--- + +### 17. ユーザー投稿取得が 2 クエリ +**ファイル:** `application/server/src/routes/api/user.ts:51-71` + +User.findOne → Post.findAll の 2 クエリ。include で 1 クエリにまとめられる。 + +--- + +### 18. FFmpeg / Kuromoji の同期的ロード +- `application/client/src/utils/load_ffmpeg.ts` — WASM コア (10〜20MB) を初回使用時にブロッキングロード +- `application/client/src/utils/negaposi_analyzer.ts` — Kuromoji 辞書を同期ロード + +--- + +### 19. 音声波形生成が毎レンダーで AudioContext を生成 +**ファイル:** `application/client/src/components/foundation/SoundWaveSVG.tsx:9-29` + +```javascript +const audioCtx = new AudioContext(); // 毎回生成 +const buffer = await audioCtx.decodeAudioData(data.slice(0)); +``` + +--- + +### 20. 公開アセットが未最適化 (365MB) + +- JPEG が WebP / AVIF に変換されていない +- GIF ファイル (25MB 超のものあり) が mp4/WebM に変換されていない +- レスポンシブ画像 (srcset) なし +- プログレッシブ JPEG でない + +--- + +## 🟡 MODERATE + +### 21. セッションミドルウェアが全リクエストで実行 +**ファイル:** `application/server/src/app.ts` + +認証不要な GET リクエスト(画像取得等)にもセッション処理が走る。 + +--- + +### 22. bodyParser の上限が 10MB +**ファイル:** `application/server/src/app.ts:14` + +```javascript +app.use(bodyParser.raw({ limit: "10mb" })); +``` + +大きなファイルアップロードを無制限に受け付ける。クライアント側で圧縮・リサイズすべき。 + +--- + +### 23. 画像アップロードが無圧縮で保存 +**ファイル:** `application/server/src/routes/api/image.ts:16-36` + +JPEG をそのまま保存。再エンコード・品質調整なし。 + +--- + +### 24. 動画・音声アップロードも無圧縮保存 +- `application/server/src/routes/api/movie.ts` — GIF 25MB をそのまま保存 +- `application/server/src/routes/api/sound.ts` — 無圧縮保存 + +--- + +--- + +## E2E テスト (`make e2e-test`) 高速化 + +### 現状 + +- テストファイル: 10ファイル、テスト数: 約30件 +- 並列実行: `fullyParallel: true`、workers: `Math.max(1, floor(cpus / 2))` +- retries: 1(全テスト共通) +- テストタイムアウト: 300s(`crok-chat` の AI 応答待ちのため) + +### ボトルネック一覧(影響度順) + +#### 🔴 HIGH: `login()` の繰り返し呼び出し + +**現状** +- `crok-chat.test.ts`: beforeEach → 2回 +- `posting.test.ts`: beforeEach → 2回 +- `dm.test.ts`: 各テストで個別に → 9回以上(2ページ同時ログインも含む) +- 合計: 約15回以上の login() 実行 + +**login() の内訳(1回あたり)** +- `page.goto("/not-found")` + `signinButton` 待ち: ~2s +- `pressSequentially` でユーザー名・パスワード入力: ~1s +- Crok リンク出現待ち: ~1s +- 合計: **~4-5s × 15回 = 60-75s** + +**対策: Playwright `storageState` で認証状態を使い回す** + +`globalSetup.ts` でログイン→`storageState` をファイルに保存し、`playwright.config.ts` の `use.storageState` に指定すると各テストのログインが不要になる。 + +注意点: +- `dm.test.ts` の WebSocket テスト(2ユーザー同時ログイン)は別途対応が必要 +- `globalSetup.ts` の DB 初期化 (`/api/v1/initialize`) は維持すること + +--- + +#### 🔴 HIGH: `pressSequentially` の多用 + +**現状** +- `login()`: ユーザー名・パスワードを1文字ずつ入力 +- `auth.test.ts`: 全フォーム入力が `pressSequentially` +- `dm.test.ts`: DMモーダルのユーザー名入力に `{ delay: 10 }` 付き + +`pressSequentially("testpass-123")` = 12文字 × 毎回 = 数百ms のオーバーヘッド + +**対策** +- ログインフォームなどバリデーションが不要な箇所は `fill()` に変更 + ```ts + await input.fill(username); // pressSequentially より高速 + ``` +- サジェスト機能が必要な箇所(Crokチャット、DMモーダル)は `pressSequentially` を維持 + +--- + +#### 🟡 MEDIUM: Crok AI応答待ち + +**現状** +- `crok-chat.test.ts` の「AIの応答が表示される」テストが最大 300s のタイムアウト +- ブラウザ内 WebLLM 推論のため、テスト側からの短縮は困難 + +**対策** +- アプリ側で CROK の `sleep(3000)` と1文字ごとの `sleep(10)` を削除する(PERFORMANCE.md #9) +- モデルを軽量なものに差し替えることで推論速度を改善(スコアへの影響要確認) + +--- + +#### 🟡 MEDIUM: `scrollEntire()` が遅い + +**現状** (`application/e2e/src/utils.ts:102-114`) +```ts +for (let i = 0; i < scrollHeight; i += 100) { // 100px刻み + window.scrollTo(0, i); + await delay(50); // 50ms待機 +} +// 往復するので2倍 +// 5000px のページ → 100回 × 50ms × 2 = 約10s +``` + +**対策** +- ステップを 500px、ディレイを 10ms に変更 → 約10s → 約2sへ短縮 + +--- + +#### 🟡 MEDIUM: Workers 数が少ない + +**現状**: `Math.floor(cpus / 2)`(8コアなら4workers) + +**対策** +```bash +E2E_WORKERS=8 make e2e-test +``` +アプリサーバーの負荷と相談しながら増やす。 + +--- + +#### 🟢 LOW: `retries: 1` が全テストに適用 + +失敗時にテストが再実行される。安定したテストには不要。 +安定したテストは `retries: 0`、不安定なテストだけ個別に設定する。 + +--- + +### E2E 並列化の可能性 + +#### 現状の並列化 + +`fullyParallel: true` は既に設定済みで、ファイル間・ファイル内ともにテストが並列実行される。 +ただし workers がデフォルトで `cpus / 2` のため、並列度が実際には低い。 + +#### テストの DB 操作分類 + +``` +読み取りのみ(並列化しやすい): + home.test.ts タイムライン閲覧、投稿クリック遷移 + terms.test.ts 利用規約閲覧 + search.test.ts 検索(DB書き込みなし) + responsive.test.ts ホーム閲覧 + user-profile.test.ts ユーザープロフィール閲覧 + post-detail.test.ts 投稿詳細閲覧 + +DB 書き込みあり(並列化で干渉リスク): + auth.test.ts 新規ユーザー作成(4テスト) + posting.test.ts 投稿作成(2テスト) + crok-chat.test.ts ログイン・Crok 操作(直接DB書き込みは少ない) + dm.test.ts DM 送信・リアルタイム操作(複数テストが同一スレッドを操作) +``` + +#### 追加できる並列化 + +**① Workers 数を増やす(簡単・すぐ試せる)** + +```bash +E2E_WORKERS= make e2e-test +``` + +制約: SQLite は write lock があるため、書き込みテストを多数並列化すると +`SQLITE_BUSY` エラーが出る可能性がある。まず読み取り専用テストで試す。 + +--- + +**② 読み取り専用テストと書き込みテストを分離実行** + +Playwright の `--grep` / `--grep-invert` や project 分割で、 +読み取り専用テストを高 workers で先に流し、書き込みテストを低 workers でシリアル実行する。 + +```ts +// playwright.config.ts +projects: [ + { + name: "read-only", + testMatch: ["**/home.test.ts", "**/search.test.ts", "**/terms.test.ts", ...], + workers: os.cpus().length, // 全コア使用 + }, + { + name: "write", + testMatch: ["**/auth.test.ts", "**/posting.test.ts", "**/dm.test.ts"], + workers: 2, // 少なめ + }, +] +``` + +--- + +**③ Playwright sharding(CI / 複数マシン向け)** + +1台のマシンでは DB を共有するため効果は限定的だが、 +CI で複数マシンに分散する場合は有効。 + +```bash +# マシン1 +npx playwright test --shard=1/3 +# マシン2 +npx playwright test --shard=2/3 +# マシン3 +npx playwright test --shard=3/3 +``` + +--- + +**④ SQLite を WAL モードに変更(読み取り並列化の改善)** + +SQLite のデフォルトは書き込み時に全テーブルをロックするが、 +WAL (Write-Ahead Logging) モードを有効にすると書き込み中でも読み取りが並列実行できる。 + +```sql +PRAGMA journal_mode=WAL; +``` + +--- + +#### 並列化できない・リスクが高い箇所 + +| テスト | 理由 | +|--------|------| +| `dm.test.ts` の WebSocket テスト | 同一 DM スレッドに複数テストが書き込む。実行順序によって既読状態・メッセージ順が変わる | +| `dm.test.ts` のソート順テスト | 他のテストの DM 送信が割り込むとソート結果が変わる | +| `auth.test.ts` の重複ユーザー名テスト | `Date.now()` でユーザー名を一意にしているため並列化可能だが、同一ミリ秒での衝突に注意 | + +--- + +### 優先対応順(E2E テスト) + +| 対策 | 実装難度 | 期待削減時間 | +|------|---------|------------| +| `storageState` で認証状態を使い回す | 中 | 60-75s | +| `pressSequentially` → `fill` | 低 | 10-20s | +| Workers 数を増やす | 低 | 並列度 × 短縮 | +| `scrollEntire` のステップ・ディレイ改善 | 低 | ~8s | +| 読み取り専用 / 書き込みテストを project 分割 | 中 | Workers 増加の恩恵を安全に受けられる | +| SQLite WAL モード | 低 | 読み取り並列化の改善 | +| CROK の sleep 削除(アプリ側) | 低 | Crok テストのみ | +| `retries` 削減 | 低 | 失敗時のみ効果 | + +--- + +--- + +## 技術制約なし:導入を検討できる技術・アプローチ + +### 🔴 HIGH インパクト + +#### A. GIF → MP4 変換(シード生成時に処理) + +**現状の問題** +- GIF ファイルは最大 25MB、`PausableMovie.tsx` がフルバイナリをダウンロードし gifler でフレームを全デコード → canvas 描画 +- CPU/メモリ負荷が高く、ハードウェアデコードも効かない + +**対策: シード生成時に FFmpeg で MP4 変換** + +サーバーはすでに `@ffmpeg/ffmpeg` を持っており、シード生成スクリプト(Node.js 側)で変換できる。 +```bash +ffmpeg -i input.gif -movflags faststart -pix_fmt yuv420p output.mp4 +``` +- GIF 25MB → MP4 1-2MB(ブラウザのハードウェアデコード対応) +- `` を `