Skip to content

🖼️ fix: Message Icon Flickering from Context-Triggered Re-renders#12489

Merged
danny-avila merged 6 commits into
devfrom
claude/inspiring-keller
Mar 31, 2026
Merged

🖼️ fix: Message Icon Flickering from Context-Triggered Re-renders#12489
danny-avila merged 6 commits into
devfrom
claude/inspiring-keller

Conversation

@danny-avila

@danny-avila danny-avila commented Mar 31, 2026

Copy link
Copy Markdown
Owner

Summary

I fixed message icon flickering caused by unnecessary re-renders of MessageIcon when AgentsMapContext or AssistantsMapContext update. After #12454 introduced aggressive memoization on MessageRender, context-triggered re-renders became the only source of visual updates for non-latest messages — making icon flicker far more noticeable than before.

  • Add a custom arePropsEqual comparator to MessageIcon's React.memo that compares only the fields the component actually renders (agent.name, agent.avatar.filepath, assistant.name, assistant.metadata.avatar) instead of relying on object reference equality.
  • Export arePropsEqual and add unit tests covering: same-data new-reference stability, field-change detection, iconData reference semantics, and undefined→defined edge cases.
  • Replace five redundant useMemo calls for trivial property accesses (agent?.name ?? '', etc.) with direct computed values — the custom comparator already gates re-renders, so these memos only added closure/dep-array overhead without providing cache hits.
  • Add JSDoc on the comparator documenting which fields are intentionally omitted (agent.id, assistant.id) and why iconData uses reference equality.

Root cause

useMessageActions (called inside memo'd MessageRender) subscribes to useAgentsMapContext() and useAssistantsMapContext(). Context subscriptions bypass React.memo, so when react-query refetches the agents/assistants list (e.g., on window focus), MessageRender re-renders regardless of its custom comparator. The agent/assistant useMemo recomputes with the new map reference, producing a new object identity even when the data is unchanged. MessageIcon's default shallow memo comparison then fails, triggering a re-render that causes visible icon flickering.

This was introduced by #12454 — not as a bug in that PR's logic, but as a newly visible symptom: before that PR, MessageParts was unmemoized, so everything re-rendered together and the icon refresh blended in. After aggressive memoization isolated non-latest messages, the context-triggered icon re-render became the only visual change, making the flicker stand out.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

  1. Open a conversation with an agent that has a custom avatar.
  2. Switch focus away from and back to the browser window (triggers react-query refetch of agents list).
  3. Observe that message icons no longer flicker on window refocus.
  4. During streaming, verify that non-latest message icons remain stable and only the latest message updates.
  5. Switch between conversations with different agent/assistant types and confirm icons render correctly.
  6. Run npx jest -- "Messages/__tests__/MessageIcon" from the client/ directory — all 9 tests pass.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

…gating

MessageIcon receives full `agent` and `assistant` objects as props from
useMessageActions, which recomputes them when AgentsMapContext or
AssistantsMapContext update (e.g., react-query refetch on window focus).
These context-triggered re-renders bypass MessageRender's React.memo,
producing new object references for agent/assistant even when the
underlying data is unchanged. The default shallow comparison in
MessageIcon's memo then fails, causing unnecessary re-renders that
manifest as visible icon flickering.

Add arePropsEqual comparator that checks only the fields MessageIcon
actually uses (name, avatar filepath, metadata avatar) instead of
object identity, so the component correctly bails out when icon-relevant
data hasn't changed.
Copilot AI review requested due to automatic review settings March 31, 2026 21:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses UI flicker in chat message icons by preventing unnecessary MessageIcon re-renders that were triggered by agents/assistants context updates (e.g., react-query refetches) even when the icon-relevant data didn’t change.

Changes:

  • Adds a custom React.memo props comparator for MessageIcon that compares only render-relevant fields (agent/assistant name + avatar fields, plus iconData by reference).
  • Extracts a MessageIconProps type to share between the component and comparator.

đź’ˇ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Export arePropsEqual so it can be tested in isolation
- Add JSDoc documenting which fields are intentionally omitted (id)
  and why iconData uses reference equality
- Replace five trivial useMemo calls (agent?.name ?? '', etc.) with
  direct computed values — the custom comparator already gates
  re-renders, so these memos only add closure/dep-array overhead
  without ever providing cache hits
- Remove unused React import
Exercise the custom memo comparator to ensure:
- New object references with same display fields are treated as equal
- Changed name or avatar filepath triggers re-render
- iconData reference change triggers re-render
- undefined→defined agent with undefined fields is treated as equal
@danny-avila danny-avila changed the title 🖼️ perf: Fix Message Icon Flickering from Context-Triggered Re-renders 🖼️ fix: Message Icon Flickering from Context-Triggered Re-renders Mar 31, 2026
@danny-avila danny-avila changed the base branch from main to dev March 31, 2026 21:57
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

iconData is a useMemo'd object from the parent, but comparing by
reference still causes unnecessary re-renders when the parent
recomputes the memo with identical primitive values. Compare all
five fields individually (endpoint, model, iconURL, modelLabel,
isCreatedByUser) for consistency with how agent/assistant are handled.
@danny-avila danny-avila merged commit 7181174 into dev Mar 31, 2026
7 checks passed
@danny-avila danny-avila deleted the claude/inspiring-keller branch March 31, 2026 22:24
yidianyiko pushed a commit to yidianyiko/LibreChat that referenced this pull request Apr 13, 2026
…nny-avila#12489)

* perf: add custom memo comparator to MessageIcon for stable re-render gating

MessageIcon receives full `agent` and `assistant` objects as props from
useMessageActions, which recomputes them when AgentsMapContext or
AssistantsMapContext update (e.g., react-query refetch on window focus).
These context-triggered re-renders bypass MessageRender's React.memo,
producing new object references for agent/assistant even when the
underlying data is unchanged. The default shallow comparison in
MessageIcon's memo then fails, causing unnecessary re-renders that
manifest as visible icon flickering.

Add arePropsEqual comparator that checks only the fields MessageIcon
actually uses (name, avatar filepath, metadata avatar) instead of
object identity, so the component correctly bails out when icon-relevant
data hasn't changed.

* refactor: export arePropsEqual, drop redundant useMemos, add JSDoc

- Export arePropsEqual so it can be tested in isolation
- Add JSDoc documenting which fields are intentionally omitted (id)
  and why iconData uses reference equality
- Replace five trivial useMemo calls (agent?.name ?? '', etc.) with
  direct computed values — the custom comparator already gates
  re-renders, so these memos only add closure/dep-array overhead
  without ever providing cache hits
- Remove unused React import

* test: add unit tests for MessageIcon arePropsEqual comparator

Exercise the custom memo comparator to ensure:
- New object references with same display fields are treated as equal
- Changed name or avatar filepath triggers re-render
- iconData reference change triggers re-render
- undefined→defined agent with undefined fields is treated as equal

* fix: replace nested ternary with if-else for eslint compliance

* test: add comment on subtle equality invariant and defined→undefined case

* perf: compare iconData by field values instead of reference

iconData is a useMemo'd object from the parent, but comparing by
reference still causes unnecessary re-renders when the parent
recomputes the memo with identical primitive values. Compare all
five fields individually (endpoint, model, iconURL, modelLabel,
isCreatedByUser) for consistency with how agent/assistant are handled.
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
…nny-avila#12489)

* perf: add custom memo comparator to MessageIcon for stable re-render gating

MessageIcon receives full `agent` and `assistant` objects as props from
useMessageActions, which recomputes them when AgentsMapContext or
AssistantsMapContext update (e.g., react-query refetch on window focus).
These context-triggered re-renders bypass MessageRender's React.memo,
producing new object references for agent/assistant even when the
underlying data is unchanged. The default shallow comparison in
MessageIcon's memo then fails, causing unnecessary re-renders that
manifest as visible icon flickering.

Add arePropsEqual comparator that checks only the fields MessageIcon
actually uses (name, avatar filepath, metadata avatar) instead of
object identity, so the component correctly bails out when icon-relevant
data hasn't changed.

* refactor: export arePropsEqual, drop redundant useMemos, add JSDoc

- Export arePropsEqual so it can be tested in isolation
- Add JSDoc documenting which fields are intentionally omitted (id)
  and why iconData uses reference equality
- Replace five trivial useMemo calls (agent?.name ?? '', etc.) with
  direct computed values — the custom comparator already gates
  re-renders, so these memos only add closure/dep-array overhead
  without ever providing cache hits
- Remove unused React import

* test: add unit tests for MessageIcon arePropsEqual comparator

Exercise the custom memo comparator to ensure:
- New object references with same display fields are treated as equal
- Changed name or avatar filepath triggers re-render
- iconData reference change triggers re-render
- undefined→defined agent with undefined fields is treated as equal

* fix: replace nested ternary with if-else for eslint compliance

* test: add comment on subtle equality invariant and defined→undefined case

* perf: compare iconData by field values instead of reference

iconData is a useMemo'd object from the parent, but comparing by
reference still causes unnecessary re-renders when the parent
recomputes the memo with identical primitive values. Compare all
five fields individually (endpoint, model, iconURL, modelLabel,
isCreatedByUser) for consistency with how agent/assistant are handled.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants