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



Если при рендеринге огромной таблицы с какой-нибудь 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)


  1. lam0x86
    13.06.2019 23:38
    +1

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


  1. SCINER
    13.06.2019 23:41
    +1

    Пример бы какой нибудь, хотябы с 100000 строк.


    1. RyDmi
      15.06.2019 01:42

      А можно пример прикладной задачи когда браузер пользователя реально надо мучать таким количеством.строк?


      1. Rulexec
        15.06.2019 23:36

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


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


        1. RyDmi
          16.06.2019 05:55

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


  1. yarkov
    14.06.2019 08:33
    -1

    > написать много логики в обработчике прокрутки, чтобы предотвратить слишком частую перерисовку, так как каждый коллбэк скролла будет вызывать setState.

    lodash.debounce


    1. Finesse
      14.06.2019 08:42
      +1

      Скорее throttle


      1. yarkov
        14.06.2019 08:56

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


    1. Yavanosta
      14.06.2019 10:54

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


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


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


  1. flamefork
    14.06.2019 08:47

    Добавлю, для справки caniuse.com/#search=IntersectionObserver


  1. frankmasonus
    14.06.2019 20:09

    Ногами не пинайте, но чем это лучше pagination с фильтром?


    1. Ogoun
      14.06.2019 22:56

      Во-первых, тренд. Во-вторых для развлекательного контента удобнее (пример на том же пикабу можно глянуть). Для аналитики или других серьезных вещей пагинация будет удобнее.


    1. demimurych
      15.06.2019 12:08

      Тем, что правильная реализация загрузки части контента(для продолжения старницы), будет всегда намного быстрее загрузки всего контента на новой странице


      1. RyDmi
        16.06.2019 06:33

        А зачем для паджинации новая страница? Данных можно грузить ровно столько же сколько и при инфинит скролле.