ワンクリックで
wsh-crok-render-optimization
// Crok AI chat rendering optimization — SSE debouncing, ChatMessage memoization, Markdown re-render prevention
// Crok AI chat rendering optimization — SSE debouncing, ChatMessage memoization, Markdown re-render prevention
DM page and flow optimization — afterSave hook, message rendering, unread count queries for WSH 2026
Home page performance optimization — Post defaultScope, SSR data reduction, lazy media hydration for WSH 2026 CaX app
Lazy loading modal containers and route components to reduce initial bundle size — CrokContainer, NewPostModalContainer
SSE streaming optimization — batching char-by-char events and debouncing React re-renders for Crok AI chat performance
Runs Visual Regression Testing (VRT) locally to prevent disqualification in Web Speed Hackathon. Captures screenshots, compares against baselines, updates snapshots, and validates visual integrity after performance optimizations. Use when optimizing WSH apps, running VRT checks, updating VRT baselines, or investigating VRT failures.
Optimizes deliberately slow web applications for maximum Lighthouse scores in Web Speed Hackathon (CyberAgent). Use when participating in WSH or performing aggressive frontend performance optimization on React/Node.js apps with SQLite backends. Covers bundle reduction, image optimization, Core Web Vitals, server tuning, and known competition traps.
| name | wsh-crok-render-optimization |
| description | Crok AI chat rendering optimization — SSE debouncing, ChatMessage memoization, Markdown re-render prevention |
Crok AI chat user flow scores TBT=0.00/25. The server sends ALL SSE chunks in a synchronous loop (no delays), causing the client to receive and process all events nearly simultaneously. Each event triggers React state updates and Markdown re-rendering.
Use this skill when optimizing the Crok AI chat performance, SSE handling, or Markdown rendering.
The current useSSE hook calls setContent() on every SSE message event. When the server sends all chunks synchronously, the client receives a burst of events that each trigger a React render.
// Current (use_sse.ts line 59):
eventSource.onmessage = (event) => {
// ...
contentRef.current = newContent;
setContent(newContent); // Triggers render on EVERY message
};
// Fix: Debounce with rAF
const rafRef = useRef<number | null>(null);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data) as T;
const isDone = options.onDone?.(data) ?? false;
if (isDone) {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
setContent(contentRef.current);
options.onComplete?.(contentRef.current);
stop();
return;
}
contentRef.current = options.onMessage(data, contentRef.current);
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null;
setContent(contentRef.current);
});
}
};
Impact: Reduces React renders from N (one per SSE event) to ~1-2 per frame. Major TBT reduction.
The server at server/src/routes/api/crok.ts sends all chunks in a synchronous for loop:
// Current: all chunks sent at once
for (let i = 0; i < lines.length; i += CHUNK_SIZE) {
res.write(...);
}
// Fix: yield event loop between chunks
for (let i = 0; i < lines.length; i += CHUNK_SIZE) {
if (res.closed) break;
res.write(...);
if (i + CHUNK_SIZE < lines.length) {
await new Promise(resolve => setImmediate(resolve));
}
}
This lets the event loop breathe between writes, preventing one massive data burst.
Impact: Spreads SSE events over time, reducing client-side event processing burst.
ChatMessage is not wrapped in React.memo, so parent re-renders cause unnecessary recalculation:
// Current:
export const ChatMessage = ({ message }: Props) => { ... };
// Fix:
export const ChatMessage = React.memo(({ message }: Props) => { ... });
The Markdown component inside AssistantMessage is the heaviest part — it parses and renders markdown with rehypeKatex and remarkGfm plugins on every render.
Impact: Prevents completed messages from re-rendering when new content streams in.
| Pitfall | Symptom | Fix |
|---|---|---|
| rAF debounce loses final content | Last chunk not displayed | Cancel rAF and do final setContent in the isDone handler |
| Server delay too long | Chat feels sluggish | Use setImmediate (not setTimeout) — yields but doesn't delay |
| memo on ChatMessage with object props | memo never skips (new object each render) | Ensure message objects have stable references (use index-based comparison) |
| Change | Visual Impact | Mitigation |
|---|---|---|
| Debouncing SSE | None — final content is same | Ensure final content renders completely |
| Server delays | None — same content, slightly different timing | N/A |
| Memoizing ChatMessage | None — same output | Run crok-chat VRT test |
client/src/hooks/use_sse.tsclient/src/containers/CrokContainer.tsxclient/src/components/crok/ChatMessage.tsxserver/src/routes/api/crok.tscrok-response.md fileuseDeferredValue but it's not sufficient when all events arrive at once