en un clic
wsh-dm-optimization
// DM page and flow optimization — afterSave hook, message rendering, unread count queries for WSH 2026
// DM page and flow optimization — afterSave hook, message rendering, unread count queries for WSH 2026
Crok AI chat rendering optimization — SSE debouncing, ChatMessage memoization, Markdown re-render prevention
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-dm-optimization |
| description | DM page and flow optimization — afterSave hook, message rendering, unread count queries for WSH 2026 |
DM一覧 scores TBT=13.80/30, DM詳細 scores TBT=10.80/30, and DM送信 flow TBT=0.00/25. Main bottleneck is the DirectMessage afterSave hook running heavy count queries synchronously.
Use this skill when optimizing DM-related pages, the DM send flow, or DirectMessage model hooks.
The current afterSave hook at server/src/models/DirectMessage.ts:75-107 performs three sequential operations:
// Current (heavy):
DirectMessage.addHook("afterSave", async (message) => {
// 1. Re-fetch the just-saved message (UNNECESSARY)
const directMessage = await DirectMessage.findByPk(message.get().id);
// 2. Fetch conversation to find receiver
const conversation = await DirectMessageConversation.findByPk(directMessage?.conversationId);
// 3. COUNT with JOIN across all conversations (EXPENSIVE)
const unreadCount = await DirectMessage.count({
distinct: true,
where: { senderId: { [Op.ne]: receiverId }, isRead: false },
include: [{ association: "conversation", where: {...}, required: true }],
});
});
Fix:
message.get() directly (step 1 eliminated)message.get().conversationId with DirectMessageConversation.unscoped().findByPk() (step 2 simplified)// Optimized count - avoid JOIN:
const conversationIds = await DirectMessageConversation.unscoped().findAll({
attributes: ['id'],
where: { [Op.or]: [{ initiatorId: receiverId }, { memberId: receiverId }] },
raw: true,
}).then(cs => cs.map(c => c.id));
const unreadCount = await DirectMessage.unscoped().count({
where: {
conversationId: { [Op.in]: conversationIds },
senderId: { [Op.ne]: receiverId },
isRead: false,
},
});
Or even simpler — defer the count computation entirely and emit the event without waiting:
// Fire-and-forget pattern:
DirectMessage.addHook("afterSave", async (message) => {
const data = message.get();
const conversation = await DirectMessageConversation.unscoped().findByPk(data.conversationId);
if (!conversation) return;
const receiverId = conversation.initiatorId === data.senderId
? conversation.memberId : conversation.initiatorId;
// Emit message event immediately (don't wait for count)
eventhub.emit(`dm:conversation/${conversation.id}:message`, message);
// Count asynchronously (non-blocking for the save operation)
setImmediate(async () => {
const unreadCount = await DirectMessage.unscoped().count({...});
eventhub.emit(`dm:unread/${receiverId}`, { unreadCount });
});
});
Impact: DM send TBT, DM detail TBT, DM list TBT all improve.
DirectMessagePage renders all messages in a .map() without individual memoization.
Impact: Minor TBT improvement for DM detail page.
| Pitfall | Symptom | Fix |
|---|---|---|
| Removing findByPk breaks sender include | Message sent without sender data | Use message.reload() only if sender data is needed in the event |
| Deferred count causes stale unread badge | Badge shows wrong number briefly | Acceptable trade-off for performance |
| unscoped() loses default ordering | Messages in wrong order | Add explicit order clause |
| Change | Visual Impact | Mitigation |
|---|---|---|
| Optimizing afterSave | None — server-side only | N/A |
| Memoizing messages | None — same output | Test DM detail VRT |
server/src/models/DirectMessage.tsserver/src/routes/api/direct_message.tsclient/src/containers/DirectMessageListContainer.tsxclient/src/containers/DirectMessageContainer.tsxDirectMessage.update() in the read endpoint