perf(messenger): оптимизация загрузки initial messages
Проблема: При открытии чата сначала загружались сообщения, затем происходила видимая прокрутка вниз - плохой UX Решение: 1. Скрываем контейнер (visibility: hidden) перед рендерингом 2. Используем DocumentFragment в renderMessages для батчевой вставки 3. Устанавливаем scrollTop ДО показа контейнера 4. Показываем контейнер - пользователь сразу видит конец диалога Преимущества: - Нет видимой прокрутки - сразу показывается конец чата - Меньше reflow (1 вместо N при forEach) - Быстрее рендеринг больших чатов (все элементы вставляются за раз) - Лучший UX - нет "прыжков" при загрузке Изменения: - userWebSocket.js: добавлено скрытие/показ контейнера, прямая установка scrollTop - messageRenderer.js: renderMessages использует DocumentFragment вместо множественных insertAdjacentHTML
This commit is contained in:
parent
1be2f3cdd4
commit
17f818656d
@ -22,19 +22,48 @@ export class MessageRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ОПТИМИЗАЦИЯ: Используем DocumentFragment для батчевой вставки
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
let lastDay = null;
|
let lastDay = null;
|
||||||
|
|
||||||
messages.forEach(msg => {
|
messages.forEach(msg => {
|
||||||
|
// Проверяем на дубликаты
|
||||||
|
const existingMessage = container.querySelector(`[data-message-id="${msg.id}"]`);
|
||||||
|
if (existingMessage) {
|
||||||
|
console.log('[MessageRenderer] Message already exists, skipping:', msg.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем разделитель дня если нужно
|
// Добавляем разделитель дня если нужно
|
||||||
const msgDate = new Date(msg.created_ts);
|
const msgDate = new Date(msg.created_ts);
|
||||||
const msgDay = this.formatDay(msgDate);
|
const msgDay = this.formatDay(msgDate);
|
||||||
|
|
||||||
if (msgDay !== lastDay) {
|
if (msgDay !== lastDay) {
|
||||||
this.addDayDivider(msgDay, container);
|
const dividerEl = document.createElement('div');
|
||||||
|
dividerEl.className = 'day-divider text-center small text-muted my-3';
|
||||||
|
dividerEl.innerHTML = `<span class="px-3 py-1 rounded-pill bg-light">${msgDay}</span>`;
|
||||||
|
fragment.appendChild(dividerEl);
|
||||||
lastDay = msgDay;
|
lastDay = msgDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderMessage(msg, container);
|
// Создаем элемент сообщения и добавляем во fragment
|
||||||
|
const isOwn = msg.author_id === this.currentUserId;
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.className = `d-flex mb-3 msg ${isOwn ? 'justify-content-end' : 'justify-content-start'}`;
|
||||||
|
messageEl.setAttribute('data-id', String(msg.id));
|
||||||
|
messageEl.setAttribute('data-message-id', String(msg.id));
|
||||||
|
messageEl.setAttribute('data-ts', String(msg.created_ts));
|
||||||
|
messageEl.setAttribute('data-author-id', String(msg.author_id || ''));
|
||||||
|
messageEl.setAttribute('data-is-edited', String(msg.is_edited || false));
|
||||||
|
messageEl.setAttribute('data-edited-at', String(msg.edited_at || ''));
|
||||||
|
messageEl.setAttribute('data-reactions', JSON.stringify(msg.reactions_summary || {}));
|
||||||
|
messageEl.innerHTML = this.buildMessageInnerHtml(msg, isOwn);
|
||||||
|
|
||||||
|
fragment.appendChild(messageEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Одна вставка всех элементов - минимизируем reflow
|
||||||
|
container.appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -273,12 +273,19 @@ export function initUserWebSocket(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[UserWS] Rendering %d initial messages via MessageRenderer', messages.length);
|
console.log('[UserWS] Rendering %d initial messages via MessageRenderer', messages.length);
|
||||||
messages.forEach(msg => {
|
|
||||||
options.messageRenderer.renderMessage(msg);
|
// ОПТИМИЗАЦИЯ: Скрываем контейнер перед рендерингом для предотвращения reflow
|
||||||
});
|
const wasVisible = state.scrollEl.style.visibility;
|
||||||
|
state.scrollEl.style.visibility = 'hidden';
|
||||||
// Скроллим вниз
|
|
||||||
scrollToBottom(true);
|
// Используем батчевый renderMessages вместо forEach для лучшей производительности
|
||||||
|
options.messageRenderer.renderMessages(messages);
|
||||||
|
|
||||||
|
// Устанавливаем scroll в конец ДО показа контейнера
|
||||||
|
state.scrollEl.scrollTop = state.scrollEl.scrollHeight;
|
||||||
|
|
||||||
|
// Показываем контейнер - пользователь сразу видит конец диалога
|
||||||
|
state.scrollEl.style.visibility = wasVisible || '';
|
||||||
|
|
||||||
// Инициализируем голосования после загрузки сообщений
|
// Инициализируем голосования после загрузки сообщений
|
||||||
if (window.chatPoll) {
|
if (window.chatPoll) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user