From 513942be0e3d4cdfc92d202853f83642f33f3e93 Mon Sep 17 00:00:00 2001 From: k-subaru by mac Date: Sat, 21 Mar 2026 00:26:42 +0900 Subject: [PATCH 01/21] Optimize client performance and stabilize DM pages --- application/client/babel.config.js | 4 +- application/client/package.json | 2 +- .../src/components/application/SearchPage.tsx | 6 +- .../direct_message/DirectMessageListPage.tsx | 10 +- .../DirectMessageNotificationBadge.tsx | 10 +- .../direct_message/DirectMessagePage.tsx | 14 +-- .../components/foundation/InfiniteScroll.tsx | 61 +++++----- .../new_post_modal/NewPostModalPage.tsx | 72 +++++++----- .../src/components/post/TranslatableText.tsx | 5 +- .../client/src/containers/AppContainer.tsx | 17 ++- .../src/containers/AuthModalContainer.tsx | 38 +++--- .../src/containers/DirectMessageContainer.tsx | 42 ++++--- .../src/containers/NewPostModalContainer.tsx | 32 +++-- .../client/src/containers/PostContainer.tsx | 4 +- .../client/src/containers/SearchContainer.tsx | 4 +- .../src/containers/TimelineContainer.tsx | 7 +- .../src/containers/UserProfileContainer.tsx | 4 +- .../client/src/hooks/use_infinite_fetch.ts | 42 +++++-- application/client/src/hooks/use_ws.ts | 29 ++++- application/client/src/index.html | 2 +- application/client/src/index.tsx | 8 +- application/client/src/utils/fetchers.ts | 109 +++++++++++++----- application/client/webpack.config.js | 29 +++-- 23 files changed, 352 insertions(+), 199 deletions(-) diff --git a/application/client/babel.config.js b/application/client/babel.config.js index c3c574591a..c74320dd37 100644 --- a/application/client/babel.config.js +++ b/application/client/babel.config.js @@ -6,14 +6,14 @@ module.exports = { { targets: "ie 11", corejs: "3", - modules: "commonjs", + modules: false, useBuiltIns: false, }, ], [ "@babel/preset-react", { - development: true, + development: process.env.NODE_ENV !== "production", runtime: "automatic", }, ], diff --git a/application/client/package.json b/application/client/package.json index 9f8e80a6a8..af2d3a5156 100644 --- a/application/client/package.json +++ b/application/client/package.json @@ -5,7 +5,7 @@ "license": "MPL-2.0", "author": "CyberAgent, Inc.", "scripts": { - "build": "NODE_ENV=development webpack", + "build": "NODE_ENV=production webpack", "typecheck": "tsc" }, "dependencies": { diff --git a/application/client/src/components/application/SearchPage.tsx b/application/client/src/components/application/SearchPage.tsx index e99045de45..519c919f1a 100644 --- a/application/client/src/components/application/SearchPage.tsx +++ b/application/client/src/components/application/SearchPage.tsx @@ -9,7 +9,6 @@ import { } from "@web-speed-hackathon-2026/client/src/search/services"; import { SearchFormData } from "@web-speed-hackathon-2026/client/src/search/types"; import { validate } from "@web-speed-hackathon-2026/client/src/search/validation"; -import { analyzeSentiment } from "@web-speed-hackathon-2026/client/src/utils/negaposi_analyzer"; import { Button } from "../foundation/Button"; @@ -53,7 +52,8 @@ const SearchPageComponent = ({ } let isMounted = true; - analyzeSentiment(parsed.keywords) + void import("@web-speed-hackathon-2026/client/src/utils/negaposi_analyzer") + .then(({ analyzeSentiment }) => analyzeSentiment(parsed.keywords)) .then((result) => { if (isMounted) { setIsNegative(result.label === "negative"); @@ -82,7 +82,7 @@ const SearchPageComponent = ({ parts.push(`${parsed.untilDate} 以前`); } return parts.join(" "); - }, [parsed]); + }, [parsed.keywords, parsed.sinceDate, parsed.untilDate]); const onSubmit = (values: SearchFormData) => { const sanitizedText = sanitizeSearchText(values.searchText.trim()); diff --git a/application/client/src/components/direct_message/DirectMessageListPage.tsx b/application/client/src/components/direct_message/DirectMessageListPage.tsx index 5a373e918e..fa671f2fb1 100644 --- a/application/client/src/components/direct_message/DirectMessageListPage.tsx +++ b/application/client/src/components/direct_message/DirectMessageListPage.tsx @@ -37,9 +37,13 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => { void loadConversations(); }, [loadConversations]); - useWs("/api/v1/dm/unread", () => { - void loadConversations(); - }); + useWs( + "/api/v1/dm/unread", + () => { + void loadConversations(); + }, + { delayMs: 5000 }, + ); if (conversations == null) { return null; diff --git a/application/client/src/components/direct_message/DirectMessageNotificationBadge.tsx b/application/client/src/components/direct_message/DirectMessageNotificationBadge.tsx index 84b62972fa..e50d40fb84 100644 --- a/application/client/src/components/direct_message/DirectMessageNotificationBadge.tsx +++ b/application/client/src/components/direct_message/DirectMessageNotificationBadge.tsx @@ -13,9 +13,13 @@ export const DirectMessageNotificationBadge = () => { const [unreadCount, updateUnreadCount] = useState(0); const displayCount = unreadCount > 99 ? "99+" : String(unreadCount); - useWs("/api/v1/dm/unread", (event: DmUnreadEvent) => { - updateUnreadCount(event.payload.unreadCount); - }); + useWs( + "/api/v1/dm/unread", + (event: DmUnreadEvent) => { + updateUnreadCount(event.payload.unreadCount); + }, + { delayMs: 5000 }, + ); if (unreadCount === 0) { return null; diff --git a/application/client/src/components/direct_message/DirectMessagePage.tsx b/application/client/src/components/direct_message/DirectMessagePage.tsx index 098c7d2894..96f124d2e6 100644 --- a/application/client/src/components/direct_message/DirectMessagePage.tsx +++ b/application/client/src/components/direct_message/DirectMessagePage.tsx @@ -43,8 +43,6 @@ export const DirectMessagePage = ({ const [text, setText] = useState(""); const textAreaRows = Math.min((text || "").split("\n").length, 5); const isInvalid = text.trim().length === 0; - const scrollHeightRef = useRef(0); - const handleChange = useCallback( (event: ChangeEvent) => { setText(event.target.value); @@ -74,16 +72,8 @@ export const DirectMessagePage = ({ ); useEffect(() => { - const id = setInterval(() => { - const height = Number(window.getComputedStyle(document.body).height.replace("px", "")); - if (height !== scrollHeightRef.current) { - scrollHeightRef.current = height; - window.scrollTo(0, height); - } - }, 1); - - return () => clearInterval(id); - }, []); + window.scrollTo(0, document.body.scrollHeight); + }, [conversation.messages.length, isPeerTyping]); if (conversationError != null) { return ( diff --git a/application/client/src/components/foundation/InfiniteScroll.tsx b/application/client/src/components/foundation/InfiniteScroll.tsx index 408f24c107..f59bde8ea0 100644 --- a/application/client/src/components/foundation/InfiniteScroll.tsx +++ b/application/client/src/components/foundation/InfiniteScroll.tsx @@ -2,48 +2,41 @@ import { ReactNode, useEffect, useRef } from "react"; interface Props { children: ReactNode; - items: any[]; + hasMore: boolean; + items: unknown[]; fetchMore: () => void; } -export const InfiniteScroll = ({ children, fetchMore, items }: Props) => { - const latestItem = items[items.length - 1]; - - const prevReachedRef = useRef(false); +export const InfiniteScroll = ({ children, fetchMore, hasMore, items }: Props) => { + const sentinelRef = useRef(null); useEffect(() => { - const handler = () => { - // 念の為 2の18乗 回、最下部かどうかを確認する - const hasReached = Array.from(Array(2 ** 18), () => { - return window.innerHeight + Math.ceil(window.scrollY) >= document.body.offsetHeight; - }).every(Boolean); - - // 画面最下部にスクロールしたタイミングで、登録したハンドラを呼び出す - if (hasReached && !prevReachedRef.current) { - // アイテムがないときは追加で読み込まない - if (latestItem !== undefined) { + const element = sentinelRef.current; + if (element == null || !hasMore || items.length === 0) { + return; + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { fetchMore(); } - } - - prevReachedRef.current = hasReached; - }; - - // 最初は実行されないので手動で呼び出す - prevReachedRef.current = false; - handler(); + }, + { + rootMargin: "200px 0px", + }, + ); - document.addEventListener("wheel", handler, { passive: false }); - document.addEventListener("touchmove", handler, { passive: false }); - document.addEventListener("resize", handler, { passive: false }); - document.addEventListener("scroll", handler, { passive: false }); + observer.observe(element); return () => { - document.removeEventListener("wheel", handler); - document.removeEventListener("touchmove", handler); - document.removeEventListener("resize", handler); - document.removeEventListener("scroll", handler); + observer.disconnect(); }; - }, [latestItem, fetchMore]); - - return <>{children}; + }, [fetchMore, hasMore, items.length]); + + return ( + <> + {children} + {hasMore ?