Что ж оно так лагает-то?
Если при рендеринге огромной таблицы с какой-нибудь transition анимацией не делать ничего дополнительно, то приложение будет лагать, а пользователь страдать.
В большинстве случаев когда мы используем event listener для создания таблицы с бесконечной прокруткой, нам не только нужно выполнить вычисления, связанные с вьюпортом и высотой строки, но и написать много логики в обработчике прокрутки, чтобы предотвратить слишком частую перерисовку, так как каждый коллбэк скролла будет вызывать setState
.
Код будет примерно таким:
componentDidMount() {
window.addEventListener('scroll', this.handleScroll)
}
handleScroll(e) {
// use window offset and boundingRect
const { ...someAttributes } = window;
const { ...someBoundingRect } = this.component
// some logic prevent re-render
if ( ... ) return;
// do some math
const newIndex = ...
// and how many rows should be rendered
this.setState({index: newIndex })
}
Но есть и другой подход к реализации бесконечной прокрутки таблицы, без знания чего-либо о значениях window или boundingRect.
Это IntersectionObserver. Определение из w3c:
Эта спецификация описывает API, который можно использовать, чтобы узнать видимость и положение элементов DOM («targets») относительно содержащехся в них элементов
При использовании этого метода вам не нужно знать высоту строки, вершину вьюпорта или вообще любое другое значение для выполнения математики.
Концепция в том, чтобы расставить анкоры с индексами в каждой контрольной точке и каждый раз, когда якорь триггерится, получать значение индекса и перерисовывать таблицу. Благодаря этому не нужно делать какой-нибудь математической магии с высотой DOM и вьюпортом.
Триггер анкора для индекса 1
Рендерим больше строк
Код с IntersectionObserver
будет примерно таким.
handleSentinel = (c) => {
if(!this.observer) {
// создаём observer
this.observer = new IntersectionObserver(
entries => {
entries.forEach(e => {
// если анкор стригерен, рендерим следующую секцию
if (e.isIntersecting) {
this.setState(
{ cursor: +e.target.getAttribute('index') }
);
}
});
},
{
root: document.querySelector('App'),
rootMargin: '-30px',
}
}
if (!c) return;
// наблюдаем за анкором
this.observer.observe(c)
}
render() {
const blockNum = 5;
return(
...
<tbody>
{MOCK_DATA.slice(0, (cursor+1) * blockNum).map(d =>
<Block>
{
// добавляем анкор в каждую контрольную точку
// например, через каждые 5 строк
d.id % blockNum === 0 ?
<span ref={this.handleSentinel} index={d.id / blockNum} />
: null
}
</Block>)}
</tbody>
...
)
}
Комментарии (14)
SCINER
13.06.2019 23:41+1Пример бы какой нибудь, хотябы с 100000 строк.
RyDmi
15.06.2019 01:42А можно пример прикладной задачи когда браузер пользователя реально надо мучать таким количеством.строк?
Rulexec
15.06.2019 23:36Есть система, где пользователи могут создавать сущности, они показываются в таблице с привычным скроллом. У пользователей есть подпользователи, которые создают себе сущности. Верхний пользователь видит их все и тоже со скроллом.
Видел до 300к строк в таблице. Хотя очевидно, что пользователь в любом случае будет фильтровать, нежели искать по алфавиту (но если забрать у них такую возможность и не предложить ничего другого адекватного, будет кипиш).
RyDmi
16.06.2019 05:55Нее, то, что пользователь работает с данными на миллионы строк это и так ясно. Вопрос в том, в какой задаче нужно именно в браузер тащить такие объемы. Даже при инфинит скролле пользователь физически не наскроллит объем более чем в несколько тысяч строк (думаю, что в реале до 1000, дальше будет фильтровать). Т.е. просто нужна качественная серверная фильтрация.
Конечно, есть задачи, в которых надо одновременно отображать несколько тысяч элементов: пример из того, с чем в последнее время работаю — таймлайн с отображением расписания, где на экране можно показать примерно 5-7К элементов за раз, но там и в ширь и в высь они идут, их можно воспринимать в таком объеме. Для грида нетормозной скролл на 5К элементов мне видится вполне достаточным. Всё имхо.
yarkov
14.06.2019 08:33-1> написать много логики в обработчике прокрутки, чтобы предотвратить слишком частую перерисовку, так как каждый коллбэк скролла будет вызывать setState.
lodash.debounceYavanosta
14.06.2019 10:54Это все равно ухудшит пользовательский опыт. Если вы добавите листнер на скролл, особенно если это НЕ passive listener браузеру придется дожидаться ответа от JS на каждый скролл что ухудшит плавность скроллинга. Даже если этот листнер будет просто делать return потому что он затроттлен. Подход с IntersectionObserver на имеет влияния на плавность скролла.
Вторая проблема: элемент может появиться на экране не только из-за скролла, но и по другим причинам. Самое частое: ресайз окна и удаление элементов из дерева, приводящее к сдвигу нижних элементов вверх. В традиционном подходе вам придется вручную вызывать какой-то хэндлер проверяющий не появилось ли что-то нового во вью порте во всех этих случаях, что опять же во первых лишняя работа, во вторых может быть нетривиально в случае если есть цепочка асинхронных вызово, и в третьих может приводить к force relayout если вы сделаете это синхронно.
Да большую часть этих проблем можно решить сделав, например класс который аккамулирует триггеры которые могут привести к необходимости догрузки данных и проверяет реальную необходимость через requestAnimationFrame + использовать только passive листенеры стролла и ресайза, но тогда вы фактически изобретете IntersectionObserver.
frankmasonus
14.06.2019 20:09Ногами не пинайте, но чем это лучше pagination с фильтром?
Ogoun
14.06.2019 22:56Во-первых, тренд. Во-вторых для развлекательного контента удобнее (пример на том же пикабу можно глянуть). Для аналитики или других серьезных вещей пагинация будет удобнее.
demimurych
15.06.2019 12:08Тем, что правильная реализация загрузки части контента(для продолжения старницы), будет всегда намного быстрее загрузки всего контента на новой странице
RyDmi
16.06.2019 06:33А зачем для паджинации новая страница? Данных можно грузить ровно столько же сколько и при инфинит скролле.
lam0x86
Как часто надо расставлять анкоры? Я правильно понимаю, что это какая-то псевдо-виртуализация? Для каждой строки таблицы создаётся контейнер, и для каждой n-ной добавляется якорь для нотификации о появлении на экране? А не поздновато ли рендерить, когда элемент уже на экране? И как это будет работать с миллионами строк?