Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
513942b
Optimize client performance and stabilize DM pages
Atlas-45 Mar 20, 2026
7af28d1
Reduce client work and fix search flow
Atlas-45 Mar 20, 2026
85433df
Gate DM websocket and read calls
Atlas-45 Mar 20, 2026
8dc6dbf
Trim payloads and add image loading hints
Atlas-45 Mar 20, 2026
0077a13
Defer media work and preserve uploaded image alt
Atlas-45 Mar 20, 2026
8f9df3d
Delay auth and audio work on landing
Atlas-45 Mar 20, 2026
bff5860
Prioritize first-view content and trim DM payloads
Atlas-45 Mar 20, 2026
297585d
Prevent stale auth fetch from overwriting sign-in
Atlas-45 Mar 20, 2026
4c4ed87
Bootstrap critical data for key pages
Atlas-45 Mar 20, 2026
d6d300d
Fix Express 5 SPA fallback route
Atlas-45 Mar 21, 2026
6e44f98
Lighten auth flow and Crok rendering
Atlas-45 Mar 21, 2026
d6d63e3
Prerender critical shells and simplify auth flow
Atlas-45 Mar 21, 2026
256b848
Serve prerendered shell for SPA routes
Atlas-45 Mar 21, 2026
96ba6ed
Hydrate client state from prerendered bootstrap
Atlas-45 Mar 21, 2026
2e17134
Keep prerendered shell visible until app mount
Atlas-45 Mar 21, 2026
27fc4a1
Enable keep-alive and static asset caching
Atlas-45 Mar 21, 2026
15b0427
Defer prerender loader import from server startup
Atlas-45 Mar 21, 2026
ac1c3e4
Align home bootstrap payload with client page size
Atlas-45 Mar 21, 2026
0e40a2c
Compress responses and stabilize DM sends
Atlas-45 Mar 21, 2026
b160822
Reduce auth flow work and shrink prerender shell
Atlas-45 Mar 21, 2026
d0abbe5
Prevent DM reload races after optimistic sends
Atlas-45 Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions application/client/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
Expand Down
2 changes: 1 addition & 1 deletion application/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 3 additions & 3 deletions application/client/src/components/application/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLTextAreaElement>) => {
setText(event.target.value);
Expand Down Expand Up @@ -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 (
Expand Down
61 changes: 27 additions & 34 deletions application/client/src/components/foundation/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(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",
},
);
Comment on lines +19 to +28
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

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 ? <div ref={sentinelRef} className="h-px w-full" aria-hidden="true" /> : null}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { MagickFormat } from "@imagemagick/magick-wasm";
import { ChangeEventHandler, FormEventHandler, useCallback, useState } from "react";

import { FontAwesomeIcon } from "@web-speed-hackathon-2026/client/src/components/foundation/FontAwesomeIcon";
import { ModalErrorMessage } from "@web-speed-hackathon-2026/client/src/components/modal/ModalErrorMessage";
import { ModalSubmitButton } from "@web-speed-hackathon-2026/client/src/components/modal/ModalSubmitButton";
import { AttachFileInputButton } from "@web-speed-hackathon-2026/client/src/components/new_post_modal/AttachFileInputButton";
import { convertImage } from "@web-speed-hackathon-2026/client/src/utils/convert_image";
import { convertMovie } from "@web-speed-hackathon-2026/client/src/utils/convert_movie";
import { convertSound } from "@web-speed-hackathon-2026/client/src/utils/convert_sound";

const MAX_UPLOAD_BYTES_LIMIT = 10 * 1024 * 1024;

Expand Down Expand Up @@ -53,24 +49,34 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm
if (isValid) {
setIsConverting(true);

Promise.all(
files.map((file) =>
convertImage(file, { extension: MagickFormat.Jpg }).then(
(blob) => new File([blob], "converted.jpg", { type: "image/jpeg" }),
void Promise.all([
import("@imagemagick/magick-wasm"),
import("@web-speed-hackathon-2026/client/src/utils/convert_image"),
])
.then(([{ MagickFormat }, { convertImage }]) =>
Promise.all(
files.map((file) =>
convertImage(file, { extension: MagickFormat.Jpg }).then(
(blob) => new File([blob], "converted.jpg", { type: "image/jpeg" }),
),
),
),
),
)
)
.then((convertedFiles) => {
setParams((params) => ({
...params,
images: convertedFiles,
movie: undefined,
sound: undefined,
}));

setIsConverting(false);
})
.catch(console.error);
.catch((error) => {
console.error(error);
setHasFileError(true);
})
.finally(() => {
setIsConverting(false);
});
}
}, []);

Expand All @@ -82,16 +88,23 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm
if (isValid) {
setIsConverting(true);

convertSound(file, { extension: "mp3" }).then((converted) => {
setParams((params) => ({
...params,
images: [],
movie: undefined,
sound: new File([converted], "converted.mp3", { type: "audio/mpeg" }),
}));

setIsConverting(false);
});
void import("@web-speed-hackathon-2026/client/src/utils/convert_sound")
.then(({ convertSound }) => convertSound(file, { extension: "mp3" }))
.then((converted) => {
setParams((params) => ({
...params,
images: [],
movie: undefined,
sound: new File([converted], "converted.mp3", { type: "audio/mpeg" }),
}));
})
.catch((error) => {
console.error(error);
setHasFileError(true);
})
.finally(() => {
setIsConverting(false);
});
}
}, []);

Expand All @@ -103,7 +116,8 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm
if (isValid) {
setIsConverting(true);

convertMovie(file, { extension: "gif", size: undefined })
void import("@web-speed-hackathon-2026/client/src/utils/convert_movie")
.then(({ convertMovie }) => convertMovie(file, { extension: "gif", size: undefined }))
.then((converted) => {
setParams((params) => ({
...params,
Expand All @@ -113,10 +127,14 @@ export const NewPostModalPage = ({ id, hasError, isLoading, onResetError, onSubm
}),
sound: undefined,
}));

setIsConverting(false);
})
.catch(console.error);
.catch((error) => {
console.error(error);
setHasFileError(true);
})
.finally(() => {
setIsConverting(false);
});
}
}, []);

Expand Down
5 changes: 3 additions & 2 deletions application/client/src/components/post/TranslatableText.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useCallback, useState } from "react";

import { createTranslator } from "@web-speed-hackathon-2026/client/src/utils/create_translator";

type State =
| { type: "idle"; text: string }
| { type: "loading" }
Expand All @@ -20,6 +18,9 @@ export const TranslatableText = ({ text }: Props) => {
(async () => {
updateState({ type: "loading" });
try {
const { createTranslator } = await import(
"@web-speed-hackathon-2026/client/src/utils/create_translator"
);
using translator = await createTranslator({
sourceLanguage: "ja",
targetLanguage: "en",
Expand Down
17 changes: 14 additions & 3 deletions application/client/src/containers/AppContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useCallback, useEffect, useId, useState } from "react";
import { Suspense, lazy, useCallback, useEffect, useId, useState } from "react";
import { Helmet, HelmetProvider } from "react-helmet";
import { Route, Routes, useLocation, useNavigate } from "react-router";

import { AppPage } from "@web-speed-hackathon-2026/client/src/components/application/AppPage";
import { AuthModalContainer } from "@web-speed-hackathon-2026/client/src/containers/AuthModalContainer";
import { CrokContainer } from "@web-speed-hackathon-2026/client/src/containers/CrokContainer";
import { DirectMessageContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageContainer";
import { DirectMessageListContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageListContainer";
import { NewPostModalContainer } from "@web-speed-hackathon-2026/client/src/containers/NewPostModalContainer";
Expand All @@ -16,6 +15,11 @@ import { TimelineContainer } from "@web-speed-hackathon-2026/client/src/containe
import { UserProfileContainer } from "@web-speed-hackathon-2026/client/src/containers/UserProfileContainer";
import { fetchJSON, sendJSON } from "@web-speed-hackathon-2026/client/src/utils/fetchers";

const LazyCrokContainer = lazy(async () => {
const module = await import("@web-speed-hackathon-2026/client/src/containers/CrokContainer");
return { default: module.CrokContainer };
});

export const AppContainer = () => {
const { pathname } = useLocation();
const navigate = useNavigate();
Expand All @@ -30,6 +34,9 @@ export const AppContainer = () => {
.then((user) => {
setActiveUser(user);
})
.catch(() => {
setActiveUser(null);
})
.finally(() => {
setIsLoadingActiveUser(false);
});
Expand Down Expand Up @@ -78,7 +85,11 @@ export const AppContainer = () => {
<Route element={<PostContainer />} path="/posts/:postId" />
<Route element={<TermContainer />} path="/terms" />
<Route
element={<CrokContainer activeUser={activeUser} authModalId={authModalId} />}
element={
<Suspense fallback={null}>
<LazyCrokContainer activeUser={activeUser} authModalId={authModalId} />
</Suspense>
}
path="/crok"
/>
<Route element={<NotFoundContainer />} path="*" />
Expand Down
Loading
Loading