Optimize client performance and stabilize DM pages#272
Optimize client performance and stabilize DM pages#272Atlas-45 wants to merge 21 commits intoCyberAgentHack:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR focuses on improving client-side performance (smaller initial bundles, better production builds, reduced CPU usage) and addressing stability/performance issues on DM-related pages.
Changes:
- Replaced jQuery-based networking with
fetchand introduced a structuredHTTPError. - Reworked infinite scrolling and pagination to use
limit/offsetrequests and anIntersectionObserversentinel. - Reduced initial bundle cost via
React.lazy/dynamic imports (modals, Crok, translator, analyzer, media converters) and enabled production webpack/Babel optimizations.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| application/client/webpack.config.js | Switches dev/prod modes, enables production defaults, adds filesystem cache in prod, removes jQuery ProvidePlugin globals. |
| application/client/src/utils/fetchers.ts | Migrates AJAX calls from jQuery to fetch, adds HTTPError and JSON/error parsing helpers. |
| application/client/src/index.tsx | Avoids rendering when #app is missing; render is no longer deferred to window.load. |
| application/client/src/index.html | Uses defer for the main bundle to improve loading behavior. |
| application/client/src/hooks/use_ws.ts | Adds delayMs/enabled options and delayed WebSocket connection creation. |
| application/client/src/hooks/use_infinite_fetch.ts | Implements server-side pagination (limit/offset) and returns hasMore. |
| application/client/src/containers/UserProfileContainer.tsx | Passes hasMore into InfiniteScroll. |
| application/client/src/containers/TimelineContainer.tsx | Passes hasMore into InfiniteScroll. |
| application/client/src/containers/SearchContainer.tsx | Handles empty query path; passes hasMore into InfiniteScroll. |
| application/client/src/containers/PostContainer.tsx | Passes hasMore into InfiniteScroll for comments. |
| application/client/src/containers/NewPostModalContainer.tsx | Lazily loads the modal page and only loads it after first open. |
| application/client/src/containers/DirectMessageContainer.tsx | Uses delayed WS connection and keeps typing/read behaviors. |
| application/client/src/containers/AuthModalContainer.tsx | Lazily loads the modal page and adapts error handling to HTTPError. |
| application/client/src/containers/AppContainer.tsx | Lazy-loads Crok route and handles /me fetch failures cleanly. |
| application/client/src/components/post/TranslatableText.tsx | Dynamically imports translator helper on demand. |
| application/client/src/components/new_post_modal/NewPostModalPage.tsx | Lazy-loads conversion dependencies and improves conversion error handling. |
| application/client/src/components/foundation/InfiniteScroll.tsx | Replaces scroll-event polling with IntersectionObserver sentinel + hasMore. |
| application/client/src/components/direct_message/DirectMessagePage.tsx | Replaces 1ms interval auto-scroll with effect-driven scroll-to-bottom. |
| application/client/src/components/direct_message/DirectMessageNotificationBadge.tsx | Delays unread WS connection. |
| application/client/src/components/direct_message/DirectMessageListPage.tsx | Delays unread WS connection used to trigger reloads. |
| application/client/src/components/application/SearchPage.tsx | Lazily imports sentiment analyzer and tightens memo deps. |
| application/client/package.json | Builds with NODE_ENV=production. |
| application/client/babel.config.js | Preserves ESM modules for tree-shaking; makes React preset development conditional. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const observer = new IntersectionObserver( | ||
| (entries) => { | ||
| if (entries.some((entry) => entry.isIntersecting)) { | ||
| fetchMore(); | ||
| } | ||
| } | ||
|
|
||
| prevReachedRef.current = hasReached; | ||
| }; | ||
|
|
||
| // 最初は実行されないので手動で呼び出す | ||
| prevReachedRef.current = false; | ||
| handler(); | ||
| }, | ||
| { | ||
| rootMargin: "200px 0px", | ||
| }, | ||
| ); |
There was a problem hiding this comment.
IntersectionObserver is not supported in IE11, but the client build still explicitly targets IE11 (see babel.config.js targets). As written, infinite scrolling will throw at runtime on IE11. Add an IntersectionObserver polyfill (loaded before this code runs) or provide a fallback path (e.g., scroll/resize listener) when window.IntersectionObserver is undefined.
| useWs( | ||
| `/api/v1/dm/${conversationId}`, | ||
| (event: DmUpdateEvent | DmTypingEvent) => { | ||
| if (event.type === "dm:conversation:message") { | ||
| void loadConversation().then(() => { | ||
| if (event.payload.sender.id !== activeUser?.id) { | ||
| setIsPeerTyping(false); | ||
| if (peerTypingTimeoutRef.current !== null) { | ||
| clearTimeout(peerTypingTimeoutRef.current); | ||
| } | ||
| peerTypingTimeoutRef.current = null; | ||
| } | ||
| peerTypingTimeoutRef.current = null; | ||
| }); | ||
| void sendRead(); | ||
| } else if (event.type === "dm:conversation:typing") { | ||
| setIsPeerTyping(true); | ||
| if (peerTypingTimeoutRef.current !== null) { | ||
| clearTimeout(peerTypingTimeoutRef.current); | ||
| } | ||
| }); | ||
| void sendRead(); | ||
| } else if (event.type === "dm:conversation:typing") { | ||
| setIsPeerTyping(true); | ||
| if (peerTypingTimeoutRef.current !== null) { | ||
| clearTimeout(peerTypingTimeoutRef.current); | ||
| peerTypingTimeoutRef.current = setTimeout(() => { | ||
| setIsPeerTyping(false); | ||
| }, TYPING_INDICATOR_DURATION_MS); | ||
| } | ||
| peerTypingTimeoutRef.current = setTimeout(() => { | ||
| setIsPeerTyping(false); | ||
| }, TYPING_INDICATOR_DURATION_MS); | ||
| } | ||
| }); | ||
| }, | ||
| { delayMs: 5000 }, | ||
| ); |
There was a problem hiding this comment.
useWs is now invoked even when activeUser is null (this component returns DirectMessageGate, but hooks still run). That will still open a WebSocket to an authenticated endpoint and immediately fail/close, and it also risks connecting with an empty conversationId (""). Since useWs now supports { enabled }, gate the connection (e.g., enabled: activeUser != null && conversationId !== "") and consider whether the 5s delayMs should apply only after the connection is actually needed (to avoid missing real-time updates right after navigation).
| export async function fetchBinary(url: string): Promise<ArrayBuffer> { | ||
| const result = await $.ajax({ | ||
| async: false, | ||
| dataType: "binary", | ||
| const response = await fetch(url, { | ||
| credentials: "same-origin", | ||
| method: "GET", | ||
| responseType: "arraybuffer", | ||
| url, | ||
| }); | ||
| return result; | ||
|
|
||
| if (!response.ok) { | ||
| return parseErrorResponse(response); | ||
| } | ||
|
|
||
| return response.arrayBuffer(); | ||
| } | ||
|
|
||
| export async function fetchJSON<T>(url: string): Promise<T> { | ||
| const result = await $.ajax({ | ||
| async: false, | ||
| dataType: "json", | ||
| const response = await fetch(url, { | ||
| credentials: "same-origin", | ||
| method: "GET", | ||
| url, | ||
| }); |
There was a problem hiding this comment.
These fetchers now rely on the browser fetch API. If the ie 11 target in babel.config.js is still intended, this will be a runtime break because fetch is not available without a polyfill (and there’s no fetch polyfill dependency/entrypoint in the client). Either add a fetch polyfill early in the bundle (before these functions are used) or update the build targets to reflect that IE11 isn’t supported.
No description provided.