From d26cc28a4bc1d8fff680f04273cc50776a8f2c18 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:44:12 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20PlayerList=20ren?= =?UTF-8?q?dering=20with=20React.memo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors `PlayerList` to use `React.memo` for the `PlayerCard` component. Includes a custom comparison function `arePlayerCardPropsEqual` to prevent unnecessary re-renders when the global game state updates but individual player data remains visually identical. This improves performance in high-frequency update scenarios (e.g. timers, voting) by avoiding re-rendering the entire player grid. Co-authored-by: WeixuanZ <39925558+WeixuanZ@users.noreply.github.com> --- .jules/bolt.md | 5 +++++ frontend/src/components/game/PlayerList.tsx | 23 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..d4b3a18 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +# Bolt's Journal + +## 2024-05-22 - [Ref Object Stability in Lists] +**Learning:** When rendering lists of items derived from a frequently updating global state (like a game state object), object references often change even if the data is identical. `React.memo`'s default shallow comparison fails here. +**Action:** Use a custom comparison function for `React.memo` that checks specific relevant fields of the data object, rather than relying on reference equality. diff --git a/frontend/src/components/game/PlayerList.tsx b/frontend/src/components/game/PlayerList.tsx index 731f6b6..4be687c 100644 --- a/frontend/src/components/game/PlayerList.tsx +++ b/frontend/src/components/game/PlayerList.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, memo } from 'react'; import { Card, Tag, theme, Button, Modal } from 'antd'; import { getRoleNameWithEmoji } from '../../utils/roleUtils'; import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; @@ -13,7 +13,7 @@ interface PlayerCardProps { onKick: () => void; } -function PlayerCard({ player, isMe, canKick, onKick }: PlayerCardProps) { +function PlayerCardComponent({ player, isMe, canKick, onKick }: PlayerCardProps) { const { token } = useToken(); const statusColor = player.is_online ? token.colorSuccess : token.colorError; @@ -127,6 +127,25 @@ function PlayerCard({ player, isMe, canKick, onKick }: PlayerCardProps) { ); } +function arePlayerCardPropsEqual(prev: PlayerCardProps, next: PlayerCardProps) { + // We explicitly ignore onKick because it's a new function on every render of PlayerList, + // but we know it's behaviorally stable (calls setKickTarget(player)). + // We compare relevant player fields to avoid re-renders when other parts of player object change (e.g. night actions). + return ( + prev.isMe === next.isMe && + prev.canKick === next.canKick && + prev.player.id === next.player.id && + prev.player.nickname === next.player.nickname && + prev.player.is_online === next.player.is_online && + prev.player.is_alive === next.player.is_alive && + prev.player.is_spectator === next.player.is_spectator && + prev.player.is_admin === next.player.is_admin && + prev.player.role === next.player.role + ); +} + +const PlayerCard = memo(PlayerCardComponent, arePlayerCardPropsEqual); + interface PlayerListProps { players: Player[]; myId: string | null;