デプロイ - #259
Conversation
- クライアントのfetch処理を同期XHRから非同期fetchへ移行し、TBT/INPを大幅改善 - 無限スクロールをIntersectionObserver+APIページングに刷新し、過剰取得を解消 - 画像バイナリ取得/変換のキャッシュ・遅延評価・objectURL化により初期描画コストを削減 - サーバ側APIのlimit/offsetデフォルト値を明示し、検索・投稿取得の重複/過剰レスポンスを整理 - 静的配信のCache-Control/Etag/Last-Modified/圧縮対応を強化し、FCP/LCP・再訪問速度を改善 - クライアントバンドル設定を本番最適化(minify, production mode)へ切替 - Crok SSE人工遅延を短縮、キャッシュクリアに対応し操作体験を改善 - 影響範囲・改善方針をdocs/performance-optimization-plan.mdに明記 - パフォーマンス要件(Lighthouseスコア/応答速度)とdocs/regulation.mdの仕様を両立するため
- パフォーマンス最適化手順書(docs/performance-optimization-plan.md)を不要のため削除 - Claude Code用ローカル設定ファイルとディレクトリを削除し、.gitignoreに追記 - Claude CLI移行によるドキュメント管理方針見直しのため
- Tailwind CSSをビルドプロセスに統合し、CDN依存を排除 - 主要なコンテナをReact.lazy+Suspenseで動的ロードし初期表示速度を向上 - WebpackのsplitChunks設定を調整し、非同期チャンク分割を有効化 - ユーザ投稿取得APIのlimit/offsetデフォルト値を設定し過剰取得を防止 - DBに主要カラムのインデックスを追加し検索・取得のレスポンスを高速化 技術的負債の解消およびUX改善のため
- CSSカスタムチェックボックスでモーダル表示状態を管理し、JSレスポンス・FCP/TBTを改善 - ルート構造をプリレンダリングに適した形にし、ナビゲーションをポータル化 - redux-form依存を排除し、検索フォームをReact状態管理に移行 - DM送信のIM・構成を見直し、作成順安定化・日本語入力のEnter誤送信を防止 - CCSSのマニフェスト/自動生成CSS・C出力を追加 - ナビ/DM/認証系のコマンド属性をlabelFor/onClick制御に統一、アクセシビリティ向上 - サーバ側DMメッセージの取得パフォーマンス改善(separate: true追加)
- 動画フォーマットをGIFからWebPに変更し、再生ロジックをcanvas+imgに刷新 - IntersectionObserverで動画・音声の遅延読み込みを実装し初期レンダリングを高速化 - babel/webpackの不要なpolyfill除去とターゲットブラウザのモダン化 - 画像のALT表示や新規投稿モーダルも遅延ロード対応しUX向上 - e2eテストもコンポーネント構造変更に追従 - デプロイワークフローや依存パッケージ(sharp追加)も一部調整 主にメディア表示の高速化・省メモリ化・アクセシビリティ向上を目的とした全面的リファクタリング
- 動画アップロード時にサムネイル画像(1フレーム目JPEG)を自動生成 - サムネイルを先行表示しLCP改善・体感速度向上 - WebP動画の圧縮率を最適化するスクリプトを追加し容量削減 - サムネイル・動画のキャッシュ期間を1日に延長し再ロードを抑制 - 既存動画ファイル・サムネイルのバッチ生成スクリプトも追加 - パフォーマンス改善およびUX向上が目的
There was a problem hiding this comment.
Pull request overview
This PR appears to target “deploy/performance” improvements across the server and client: adding DB indexes and caching/compression on the server, and shipping a more production-oriented client bundle with additional lazy-loading and UI/CSS changes.
Changes:
- Server: add SQLite indexes, adjust static asset caching headers, add response compression, and optimize DM/search query patterns.
- Client: switch to production webpack output (minify/split chunks), migrate fetchers from jQuery AJAX to
fetch, add lazy-loading/infinite-scroll improvements, and refactor modal rendering to a CSS checkbox-driven shell. - Media/CSS: introduce Sharp-based thumbnail generation/scripts and migrate Tailwind usage to PostCSS.
Reviewed changes
Copilot reviewed 49 out of 112 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| application/server/src/sequelize.ts | Adds CREATE INDEX IF NOT EXISTS calls during DB init. |
| application/server/src/routes/static.ts | Adds cache-control / ETag / max-age behaviors for static paths. |
| application/server/src/routes/api/user.ts | Defaults pagination params for user posts endpoint. |
| application/server/src/routes/api/search.ts | Reworks search to fetch IDs first, then hydrate posts. |
| application/server/src/routes/api/post.ts | Defaults pagination params for posts/comments endpoints. |
| application/server/src/routes/api/movie.ts | Adds thumbnail generation via sharp on upload. |
| application/server/src/routes/api/initialize.ts | Clears crok suggestions cache during initialize. |
| application/server/src/routes/api/direct_message.ts | Changes DM list query + emits eventhub message events on send. |
| application/server/src/routes/api/crok.ts | Adds suggestions caching + tweaks SSE headers/delays. |
| application/server/src/models/DirectMessageConversation.ts | Sets separate: true for default-scope messages include. |
| application/server/src/app.ts | Adds compression() and updates API cache-control behavior. |
| application/server/scripts/recompress-movies.mjs | Adds a Sharp-based movie recompression script. |
| application/server/scripts/generate-movie-thumbnails.mjs | Adds a Sharp-based thumbnail generation script. |
| application/server/package.json | Adds compression, sharp, and @types/compression. |
| application/public/movies/b3998a47-ee87-483e-acf1-8e5b69c8527a_thumb.jpg | Adds a pre-generated movie thumbnail asset. |
| application/public/movies/1b558288-6ec6-4ece-a9b8-4259379b7489_thumb.jpg | Adds a pre-generated movie thumbnail asset. |
| application/pnpm-lock.yaml | Lockfile updates for added dependencies (sharp/compression/tailwind/etc.). |
| application/e2e/src/dm.test.ts | Increases timeouts for DM E2E stability. |
| application/client/webpack.config.js | Switches to production mode + Terser + chunk splitting + source-map. |
| application/client/src/utils/get_path.ts | Changes movie path to .webp and adds thumbnail path helper. |
| application/client/src/utils/fetchers.ts | Replaces jQuery AJAX with fetch + centralized HttpError handling. |
| application/client/src/utils/fetch_binary_cached.ts | Adds an in-memory promise cache for binary fetches. |
| application/client/src/tailwind.css | Adds Tailwind v4 CSS entry with theme/utilities. |
| application/client/src/index.tsx | Renders React root immediately (no window.load listener). |
| application/client/src/index.html | Adds defer, removes browser Tailwind CDN, introduces CCSS shell markup. |
| application/client/src/index.css | Imports tailwind + generated CSS, adds modal overlay CSS, adjusts font-display. |
| application/client/src/hooks/use_object_url.ts | Adds a hook for object URL creation/cleanup. |
| application/client/src/hooks/use_infinite_fetch.ts | Changes infinite fetch to true pagination (limit/offset) + request token logic. |
| application/client/src/hooks/use_fetch.ts | Adds enabled option to conditionally skip fetching. |
| application/client/src/containers/NewPostModalContainer.tsx | Refactors modal open/close to checkbox-state + lazy-loads modal page. |
| application/client/src/containers/AuthModalContainer.tsx | Refactors auth modal to checkbox-state overlay. |
| application/client/src/containers/AppContainer.tsx | Portals Navigation into pre-rendered shell + lazy-loads some routes. |
| application/client/src/components/timeline/TimelineItem.tsx | Adjusts click filtering logic around anchors/buttons. |
| application/client/src/components/post/TranslatableText.tsx | Lazy-loads translator module on demand. |
| application/client/src/components/foundation/SoundPlayer.tsx | Defers binary fetch until near-viewport using IntersectionObserver. |
| application/client/src/components/foundation/PausableMovie.tsx | Replaces GIF decoding with animated WebP + canvas draw loop + thumbnail. |
| application/client/src/components/foundation/InfiniteScroll.tsx | Replaces scroll listeners with IntersectionObserver sentinel. |
| application/client/src/components/foundation/CoveredImage.tsx | Loads <img> directly and fetches EXIF alt text only on demand. |
| application/client/src/components/foundation/AspectRatioBox.tsx | Uses CSS aspect-ratio instead of JS resize measurement. |
| application/client/src/components/direct_message/DirectMessagePage.tsx | IME-friendly submit behavior + message ordering adjustments. |
| application/client/src/components/direct_message/DirectMessageGate.tsx | Opens auth modal via checkbox state instead of command/commandfor. |
| application/client/src/components/crok/CrokGate.tsx | Opens auth modal via checkbox state instead of command/commandfor. |
| application/client/src/components/application/SearchPage.tsx | Replaces redux-form with local state validation/submit handling. |
| application/client/src/components/application/NavigationItem.tsx | Adds checkbox-toggle behavior (labelFor) for modal triggers. |
| application/client/src/components/application/Navigation.tsx | Switches modal triggers to labelFor checkbox toggles. |
| application/client/src/ccss/ui.generated.css | Adds generated CCSS CSS artifacts and shared .ccss-state-input rules. |
| application/client/src/ccss/ui.generated.c | Adds generated CCSS C renderer artifact. |
| application/client/src/ccss/ccss.manifest.json | Adds generated CCSS manifest artifact. |
| application/client/src/ccss/AppShell.tsx | Adds CCSS source component (currently not wired into app runtime). |
| application/client/postcss.config.js | Adds Tailwind v4 PostCSS plugin. |
| application/client/package.json | Adds tailwindcss + @tailwindcss/postcss dev dependencies. |
| application/client/babel.config.js | Targets modern Chrome, disables commonjs transform, removes dev-only preset flag. |
| .gitignore | Ignores .claude/ local settings. |
Files not reviewed (1)
- application/pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
application/client/src/components/foundation/SoundPlayer.tsx:40
blobUrlis created viaURL.createObjectURL(...)but never revoked. As users scroll through many sounds, this will leak memory. Use a cleanup (useEffectthat revokes the previous URL) or reuse the newuseObjectUrlhook to manage object URL lifecycle.
const blobUrl = useMemo(() => {
return data !== null ? URL.createObjectURL(new Blob([data])) : null;
}, [data]);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| association: "messages", | ||
| required: true, | ||
| limit: 1, | ||
| order: [["createdAt", "DESC"]], | ||
| include: [{ association: "sender", include: [{ association: "profileImage" }] }], | ||
| }, |
There was a problem hiding this comment.
The messages include uses limit: 1 + order but does not set separate: true. In Sequelize, limiting a hasMany include generally requires separate: true; otherwise the limit may be ignored or the query shape can be incorrect. Add separate: true (and ideally keep attributes minimal) so each conversation reliably loads only the latest message.
| const cache = new Map<string, Promise<ArrayBuffer>>(); | ||
|
|
||
| export function fetchBinaryCached(url: string): Promise<ArrayBuffer> { | ||
| if (!cache.has(url)) { | ||
| cache.set(url, fetchBinary(url)); | ||
| } | ||
| return cache.get(url)!; |
There was a problem hiding this comment.
fetchBinaryCached caches the Promise permanently, including rejection. If the first request fails (network/server hiccup), every subsequent call will reuse the rejected promise and never retry. Consider deleting the cache entry on rejection (or caching only successful results).
| if (searchTerm) { | ||
| postsByUser = await Post.findAll({ | ||
| include: [ | ||
| { | ||
| association: "user", | ||
| attributes: { exclude: ["profileImageId"] }, | ||
| include: [{ association: "profileImage" }], | ||
| required: true, | ||
| where: { | ||
| [Op.or]: [{ username: { [Op.like]: searchTerm } }, { name: { [Op.like]: searchTerm } }], | ||
| }, | ||
| }, | ||
| { | ||
| association: "images", | ||
| through: { attributes: [] }, | ||
| }, | ||
| { association: "movie" }, | ||
| { association: "sound" }, | ||
| ], | ||
| limit, | ||
| offset, | ||
| where: dateWhere, | ||
| const users = await User.unscoped().findAll({ | ||
| attributes: ["id"], | ||
| where: { | ||
| [Op.or]: [{ username: { [Op.like]: searchTerm } }, { name: { [Op.like]: searchTerm } }], | ||
| }, | ||
| }); | ||
| const userIds = users.map((user) => user.id); | ||
|
|
||
| where[Op.or] = [ | ||
| { text: { [Op.like]: searchTerm } }, | ||
| ...(userIds.length > 0 ? [{ userId: { [Op.in]: userIds } }] : []), | ||
| ]; |
There was a problem hiding this comment.
This search implementation loads all matching user IDs into memory first (User.unscoped().findAll(...)), then uses userId IN (...). For broad keywords this can create very large IN lists and extra DB roundtrips. Consider rewriting as a single query (join Post↔User with an OR across Post.text/User.username/User.name, or use a subquery) to keep the work in the DB and avoid large IN clauses.
| // 変換した動画の拡張子 | ||
| const EXTENSION = "gif"; | ||
|
|
||
| export const movieRouter = Router(); | ||
|
|
||
| movieRouter.post("/movies", async (req, res) => { | ||
| if (req.session.userId === undefined) { | ||
| throw new httpErrors.Unauthorized(); | ||
| } | ||
| if (Buffer.isBuffer(req.body) === false) { | ||
| throw new httpErrors.BadRequest(); | ||
| } | ||
|
|
||
| const type = await fileTypeFromBuffer(req.body); | ||
| if (type === undefined || type.ext !== EXTENSION) { | ||
| throw new httpErrors.BadRequest("Invalid file type"); | ||
| } | ||
|
|
||
| const movieId = uuidv4(); | ||
|
|
||
| const filePath = path.resolve(UPLOAD_PATH, `./movies/${movieId}.${EXTENSION}`); | ||
| await fs.mkdir(path.resolve(UPLOAD_PATH, "movies"), { recursive: true }); |
There was a problem hiding this comment.
EXTENSION is still hard-coded to "gif" and the upload handler rejects anything else, but the client-side paths were changed to request .webp movies. This will cause newly uploaded movies (and likely playback) to break. Update the server to accept/store the same format the client requests (and adjust validation + filePath/thumbnail generation accordingly), or revert the client path change.
| void fetchJSON<Models.User>("/api/v1/me").then((user) => { | ||
| setActiveUser(user); | ||
| }); |
There was a problem hiding this comment.
fetchJSON("/api/v1/me") can throw now (the new fetcher throws HttpError on non-2xx). This effect has no .catch, so unauthenticated users (401) will cause an unhandled promise rejection and activeUser will never be set. Add error handling (e.g., catch 401 and keep activeUser as null, optionally handling other statuses).
| void fetchJSON<Models.User>("/api/v1/me").then((user) => { | |
| setActiveUser(user); | |
| }); | |
| void fetchJSON<Models.User>("/api/v1/me") | |
| .then((user) => { | |
| setActiveUser(user); | |
| }) | |
| .catch((error: unknown) => { | |
| // If unauthenticated, keep activeUser as null and swallow the error. | |
| if ( | |
| error && | |
| typeof error === "object" && | |
| "status" in error && | |
| (error as { status?: number }).status === 401 | |
| ) { | |
| setActiveUser(null); | |
| return; | |
| } | |
| // For other errors, log them so they don't become unhandled rejections. | |
| // eslint-disable-next-line no-console | |
| console.error("Failed to fetch active user:", error); | |
| }); |
| "schemaVersion": 1, | ||
| "compiler": "@ccss/compiler", | ||
| "generatedAt": "2026-03-20T04:48:57.769Z", | ||
| "sourcePath": "C:\\.program\\web-speed-hackathon-2026\\application\\client\\src\\ccss\\AppShell.tsx", |
There was a problem hiding this comment.
This generated manifest embeds a developer-local absolute Windows path (sourcePath: "C:\\..."). Committing machine-specific paths makes the artifact non-portable and creates noisy diffs across environments. Prefer omitting sourcePath or storing a repo-relative path.
| "sourcePath": "C:\\.program\\web-speed-hackathon-2026\\application\\client\\src\\ccss\\AppShell.tsx", | |
| "sourcePath": "application/client/src/ccss/AppShell.tsx", |
| export function getMoviePath(movieId: string): string { | ||
| return `/movies/${movieId}.gif`; | ||
| return `/movies/${movieId}.webp`; | ||
| } | ||
|
|
||
| export function getMovieThumbnailPath(movieId: string): string { | ||
| return `/movies/${movieId}_thumb.jpg`; |
There was a problem hiding this comment.
getMoviePath() now returns a .webp URL, but the server movie upload endpoint currently stores/validates gif files (see EXTENSION = "gif" in the server). This mismatch will break movie rendering/download. Align the client path with the server’s stored extension (or update the server to use .webp).
| const handleChange = useCallback( | ||
| (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const val = e.target.value; | ||
| setSearchText(val); |
There was a problem hiding this comment.
handleChange uses the type React.ChangeEvent<HTMLInputElement> but React isn’t imported in this file. This will fail type-checking unless React is globally available. Use ChangeEvent<HTMLInputElement> and import it from react, or import type React if you want to keep the namespace reference.
| app.use(compression()); | ||
| app.use(sessionMiddleware); |
There was a problem hiding this comment.
Applying compression() globally can break or degrade Server-Sent Events streaming (e.g. /api/v1/crok uses text/event-stream and relies on immediate flushing). Consider disabling compression for SSE responses (custom filter that returns false for text/event-stream) or applying compression only to routes where it’s safe.
- アカウントメニューのドロップダウン表示をCCSS方式(チェックボックス+CSS)に変更し、UIの一貫性とアクセシビリティを向上 - 動画再生をCanvas/rAF方式からネイティブvideo要素に切り替え、パフォーマンスと可搬性を改善 - JS未読み込み時のナビゲーションの静的フォールバックを追加し、FCPを改善 - クラス名やスタイルを整理し、メンテナンス性を向上
- サムネイル画像が正しく前面に表示されるようz-indexを付与 - ローディング中のUI表示崩れを防止する目的
- モーダルのラッパーをdivからdialog要素に置き換え - アクセシビリティ向上やネイティブUI挙動の活用を目的とする
- index.htmlに挿入された静的ナビゲーションをReact Portalで置き換えるため、初回レンダリング時に自動で削除 - ナビゲーションの重複表示を防止し、動的なUI制御を徹底するため
- index.htmlにフォントとSVGスプライトのpreloadヒントを追加(FCP/LCP改善) - index.cssにCSS :has()セレクタで静的fallback navを非表示にするルールを追加 - AppContainerの静的nav削除useEffectを削除(JS DOM操作→CSS制御に置換でINP改善) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- チェックボックス+:has()によるモーダル・メニュー表示制御を削除し、ReactのuseState/useRef+Modalコンポーネントによる管理に変更 - 関連するボタン、トリガー属性(command/commandfor)へ移行し、DOM依存の副作用を解消 - コードの可読性・保守性向上およびSSR/FCP最適化のため - 既存のCSSとinput要素を整理し不要な依存を削除
- 検索画面でローディング中表示と「結果なし」表示の条件を分離 - ネガポジ判定モジュールの動的importで初期表示速度を改善 - CoveredImageの画像遅延読み込み属性を削除し意図しない描画遅延を解消 - ナビゲーションアイテムのボタンでモーダル表示コマンドを直接処理 - 画像APIで複数拡張子(jpg, png, webp, gif)アップロードを許可しつつ保存は常にjpgに統一 - kuromojiのTokenizer生成をシングルトン化しリソース効率向上 UI/UXの向上とAPIの柔軟性改善が主な目的
- 画像変換時に既にJPEGの場合は変換処理をスキップし、高速化 - ImageMagick/FFmpegのwasmバイナリをビルド時に出力し、動的importからURL参照方式に変更 - 画像のaltテキストをuseEffectで自動取得し、アクセシビリティとテスト容易性を向上 - 主要コンテナを遅延読込(lazy import)に統一し、初回ロードを最適化 背景: - 変換不要な画像の処理コスト削減とUIレスポンス向上のため - wasmバイナリのimportに起因する初期ロード遅延やビルドエラーを解消するため - アクセシビリティツール・テストツールへの対応強化のため
- 正規表現の見直しにより日付抽出ロジックを簡潔化 - 冗長な部分関数と複雑なパターンマッチの削除で可読性を向上 - 保守性向上および将来の拡張性を考慮
- 各ロード時に「読込中...」メッセージを追加し、UXを向上 - 画像・動画・音声の変換モジュールを動的importに変更し初期表示速度を改善 - 画像alt取得処理をrequestIdleCallbackで遅延実行しTBTを最小化 - 不要なimport削除などコード整理 パフォーマンス最適化とユーザーへのフィードバック強化が主な目的
- index.html: #ccss-nav-rootの静的フォールバックnavを削除。ReactポータルがDOMに追加するnav+nav状態でPlaywrightが非表示の先頭navを誤検出し toBeVisible() が失敗していたため - index.css: #ccss-nav-root:has(nav+nav)のdisplay:noneルールを削除(静的nav削除に伴い不要) - PausableMovie: <video src="*.webp">からcanvas+requestAnimationFrameによるアニメーションWebP描画に変更。animated WebPはHTML5 videoで再生不可(readyState=0)のためwaitForVisibleMediaがタイムアウトしていた。canvasはdynamicMediaMaskでマスクされVRTも安定する 全52件のE2Eテスト通過確認済み Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 画像変換処理をWeb Workerに分離し、メインスレッドのブロッキングを解消 - lodash依存を排除し、波形生成処理のパフォーマンスを最適化 - Suspenseのフォールバック表示やSSEの更新優先度制御など、UX改善とパフォーマンス向上を同時に実現 - 画像変換時のEXIF復元処理もWorker側に実装し、安定性向上 - LCP候補画像の常時描画など、パフォーマンス計測指標への配慮を追加
- 音声波形解析処理をWeb Workerに分離し、UIスレッドの負荷を軽減 - レスポンス改善と大容量音声データ対応のため - アプリケーションのルートレイアウトにヘッダー追加、flexレイアウト最適化 - 画像メタデータ読み込み部分の動的import方法を変更
- moment.jsの利用を廃止し、独自のformat_dateユーティリティで日付・時刻・相対時間表示を統一 - バンドルサイズ削減・パフォーマンス向上・保守性向上を目的とする - サーバ側画像アップロード・配信時にsharpによるリサイズ・圧縮・WebP変換・キャッシュ対応を追加しLCP改善
- 初回アクセス時、index.htmlに最新投稿データを埋め込むことでタイムラインの即時描画を実現 - クライアント側はAPI往復なしで初期投稿を表示しLCPを大幅短縮 - サーバ側でindex.htmlをキャッシュし効率化 - redux-form reducerを遅延ロード化し初期バンドル縮小 - 不要なフォントとライブラリのプリロード・依存を削除し軽量化 - 画像最適化時にメタデータを保持、一部画像サイズを用途別に最適化
- チャット入力時のkuromojiトークナイザー初期化をマウント時から初回入力時へ遅延し、ページロード時の17MB辞書DLを回避 - BM25類似度検索のlodash依存を削減しパフォーマンス改善 - 画像alt自動抽出処理の非同期化でTBTの改善を意識 - DMページのメッセージリスト描画をuseMemoで最適化&自動スクロールをリファクタ - 投稿初期取得件数を30→5件に制限し初期表示を高速化 - パスワードバリデーションのロジック修正(記号必須判定を修正) パフォーマンス改善・初期表示高速化・UX向上を目的とした複合的な修正
No description provided.