Description
The Mail module suffers from a severe performance degradation (an N+1 query problem) when rendering the inbox message list. This causes significant latency when loading the inbox or triggering inbox updates (such as during the /mail/mail/reply or update-entries AJAX requests).
If a user has many messages in their inbox, the redundant database queries can add several seconds of dead waiting time to the request, which also causes secondary frontend issues like AJAX race conditions (duplicate or out-of-order messages).
Root Cause Analysis
When the inbox renders, views/inbox.php loops through UserMessage objects and passes them to the InboxMessagePreview widget. The UserMessage model already has the pinned status loaded in memory from the main optimized query (UserMessage::findByUser()).
However, in protected/modules/mail/widgets/InboxMessagePreview.php, the run() method does not pass the $userMessage object to the view context.
Consequently, in views/inboxMessagePreview.php, the view calls $message->getPinIcon(). This triggers the following chain:
Message::getPinIcon() calls $this->isPinned()
Message::isPinned() calls $this->getUserMessage($userId)
Message::getUserMessage() executes a brand new UserMessage::findOne(['user_id' => $userId, 'message_id' => $this->id]) database query.
If a user has 30 messages in their inbox, this results in 1 initial query + 30 redundant findOne() queries just to check if messages are pinned.
Steps to Reproduce
- Have a user with multiple conversations in their inbox.
- Enable the MariaDB slow query log (e.g., set
long_query_time = 1).
- Load the inbox or trigger an inbox update (e.g., sending a reply which updates the UI/inbox state).
- Observe multiple
SELECT * FROM user_message WHERE user_id=X AND message_id=Y queries executing sequentially in the slow log.
Proposed Fix
We need to pass the already-loaded $userMessage object into the view context to avoid the redundant database lookup.
File 1: protected/modules/mail/widgets/InboxMessagePreview.php**
Update the run() method to pass the userMessage object to the view:
public function run()
{
if ($this->getLastEntry() === null) {
return '';
}
return $this->render('inboxMessagePreview', [
'message' => $this->userMessage->message,
'userMessage' => $this->userMessage, // <-- Pass the loaded relation
'messageTitle' => $this->getMessageTitle(),
'messageText' => $this->getMessagePreview(),
'messageTime' => $this->getMessageTime(),
'lastParticipant' => $this->lastParticipant(),
'options' => $this->getOptions(),
]);
}
File 2: protected/modules/mail/widgets/views/inboxMessagePreview.php**
Update the view to use the passed $userMessage->pinned state directly from memory instead of querying the DB via $message->getPinIcon():
<!-- Change this: -->
<!-- <?= Html::encode($messageTitle) . ' ' . $message->getPinIcon() ?> -->
<!-- To this: -->
<?= Html::encode($messageTitle) ?>
<?php if ($userMessage->pinned): ?>
<?= \humhub\modules\ui\icon\widgets\Icon::get('map-pin')
->tooltip(Yii::t('MailModule.base', 'Pinned'))
->color('var(--bs-danger)') ?>
<?php endif; ?>
Impact
Fixing this N+1 query problem will drastically reduce the database load and response time for inbox rendering. By dropping dozens of unnecessary DB hits, the backend latency for requests like /mail/reply should drop significantly, naturally resolving the frontend AJAX race conditions caused by the 9+ second response times.
Description
The Mail module suffers from a severe performance degradation (an N+1 query problem) when rendering the inbox message list. This causes significant latency when loading the inbox or triggering inbox updates (such as during the
/mail/mail/replyorupdate-entriesAJAX requests).If a user has many messages in their inbox, the redundant database queries can add several seconds of dead waiting time to the request, which also causes secondary frontend issues like AJAX race conditions (duplicate or out-of-order messages).
Root Cause Analysis
When the inbox renders,
views/inbox.phploops throughUserMessageobjects and passes them to theInboxMessagePreviewwidget. TheUserMessagemodel already has thepinnedstatus loaded in memory from the main optimized query (UserMessage::findByUser()).However, in
protected/modules/mail/widgets/InboxMessagePreview.php, therun()method does not pass the$userMessageobject to the view context.Consequently, in
views/inboxMessagePreview.php, the view calls$message->getPinIcon(). This triggers the following chain:Message::getPinIcon()calls$this->isPinned()Message::isPinned()calls$this->getUserMessage($userId)Message::getUserMessage()executes a brand newUserMessage::findOne(['user_id' => $userId, 'message_id' => $this->id])database query.If a user has 30 messages in their inbox, this results in 1 initial query + 30 redundant
findOne()queries just to check if messages are pinned.Steps to Reproduce
long_query_time = 1).SELECT * FROM user_message WHERE user_id=X AND message_id=Yqueries executing sequentially in the slow log.Proposed Fix
We need to pass the already-loaded
$userMessageobject into the view context to avoid the redundant database lookup.File 1:
protected/modules/mail/widgets/InboxMessagePreview.php**Update the
run()method to pass theuserMessageobject to the view:File 2:
protected/modules/mail/widgets/views/inboxMessagePreview.php**Update the view to use the passed
$userMessage->pinnedstate directly from memory instead of querying the DB via$message->getPinIcon():Impact
Fixing this N+1 query problem will drastically reduce the database load and response time for inbox rendering. By dropping dozens of unnecessary DB hits, the backend latency for requests like
/mail/replyshould drop significantly, naturally resolving the frontend AJAX race conditions caused by the 9+ second response times.