Делаем таблицу с бесконечной прокруткой без event listener

Автор оригинала: Frank Tsai
  • Перевод

Что ж оно так лагает-то?



Если при рендеринге огромной таблицы с какой-нибудь 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> 
  ...
  )
}
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 16

    +1
    Как часто надо расставлять анкоры? Я правильно понимаю, что это какая-то псевдо-виртуализация? Для каждой строки таблицы создаётся контейнер, и для каждой n-ной добавляется якорь для нотификации о появлении на экране? А не поздновато ли рендерить, когда элемент уже на экране? И как это будет работать с миллионами строк?
      0

      Можно вообще не расставлять — блок с контентом position relative, нижний ватермарк — position absolute, bottom 200px (или какой буфер вам надо) его и обсервить.

      +1
      Пример бы какой нибудь, хотябы с 100000 строк.
        0
        А можно пример прикладной задачи когда браузер пользователя реально надо мучать таким количеством.строк?
          0

          Есть система, где пользователи могут создавать сущности, они показываются в таблице с привычным скроллом. У пользователей есть подпользователи, которые создают себе сущности. Верхний пользователь видит их все и тоже со скроллом.


          Видел до 300к строк в таблице. Хотя очевидно, что пользователь в любом случае будет фильтровать, нежели искать по алфавиту (но если забрать у них такую возможность и не предложить ничего другого адекватного, будет кипиш).

            0
            Нее, то, что пользователь работает с данными на миллионы строк это и так ясно. Вопрос в том, в какой задаче нужно именно в браузер тащить такие объемы. Даже при инфинит скролле пользователь физически не наскроллит объем более чем в несколько тысяч строк (думаю, что в реале до 1000, дальше будет фильтровать). Т.е. просто нужна качественная серверная фильтрация.
            Конечно, есть задачи, в которых надо одновременно отображать несколько тысяч элементов: пример из того, с чем в последнее время работаю — таймлайн с отображением расписания, где на экране можно показать примерно 5-7К элементов за раз, но там и в ширь и в высь они идут, их можно воспринимать в таком объеме. Для грида нетормозной скролл на 5К элементов мне видится вполне достаточным. Всё имхо.
            0
            Зачем тогда создавать таблицу с бесконечной прокруткой?
          –1
          > написать много логики в обработчике прокрутки, чтобы предотвратить слишком частую перерисовку, так как каждый коллбэк скролла будет вызывать setState.

          lodash.debounce
            +1

            Скорее throttle

              0

              Хм, да, для прокрутки таблицы пожалуй throttle.

              0

              Это все равно ухудшит пользовательский опыт. Если вы добавите листнер на скролл, особенно если это НЕ passive listener браузеру придется дожидаться ответа от JS на каждый скролл что ухудшит плавность скроллинга. Даже если этот листнер будет просто делать return потому что он затроттлен. Подход с IntersectionObserver на имеет влияния на плавность скролла.


              Вторая проблема: элемент может появиться на экране не только из-за скролла, но и по другим причинам. Самое частое: ресайз окна и удаление элементов из дерева, приводящее к сдвигу нижних элементов вверх. В традиционном подходе вам придется вручную вызывать какой-то хэндлер проверяющий не появилось ли что-то нового во вью порте во всех этих случаях, что опять же во первых лишняя работа, во вторых может быть нетривиально в случае если есть цепочка асинхронных вызово, и в третьих может приводить к force relayout если вы сделаете это синхронно.


              Да большую часть этих проблем можно решить сделав, например класс который аккамулирует триггеры которые могут привести к необходимости догрузки данных и проверяет реальную необходимость через requestAnimationFrame + использовать только passive листенеры стролла и ресайза, но тогда вы фактически изобретете IntersectionObserver.

              0
              Добавлю, для справки caniuse.com/#search=IntersectionObserver
                0
                Ногами не пинайте, но чем это лучше pagination с фильтром?
                  0
                  Во-первых, тренд. Во-вторых для развлекательного контента удобнее (пример на том же пикабу можно глянуть). Для аналитики или других серьезных вещей пагинация будет удобнее.
                    0
                    Тем, что правильная реализация загрузки части контента(для продолжения старницы), будет всегда намного быстрее загрузки всего контента на новой странице
                      0
                      А зачем для паджинации новая страница? Данных можно грузить ровно столько же сколько и при инфинит скролле.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое