Повышение производительности iScroll на большой таблице

Я обновляю заголовок таблицы и ее первые позиции столбцов программно на основе того, как пользователь прокручивается, чтобы поддерживать выравнивание.

Проблема, с которой я сталкиваюсь, заключается в том, что, как только мои наборы данных становятся достаточно большими, прокрутка становится все более и более прерывистой/менее плавной.

Соответствующий код находится в самом низу скрипта:

iScroll.on('scroll', function(){
    var pos = $('#scroller').position();
    $('#pos').text('pos.left=' + pos.left + ' pos.top=' + pos.top);

    // code to hold first row and first column
    $('#scroller th:nth-child(1)').css({top: (-pos.top), left: (-pos.left), position:'relative'});
    $('#scroller th:nth-child(n+1)').css({top: (-pos.top), position:'relative'});

    // this seems to be the most expensive operation:
    $('#scroller td:nth-child(1)').css({left: (-pos.left), position:'relative'});
});

Я знаю, что это может быть написано намного более эффективно, кэшируя элементы и так далее. Например, я попытался сохранить элементы в массиве и обновить их позицию в режиме "vanilla":

headerElements[i].style.left = left + 'px'; // etc...

Независимо от того, насколько быстро я делаю обратный вызов, я все еще не доволен результатом. Есть ли у вас какие-либо предложения?

https://jsfiddle.net/0qv1kjac/16/

Ответы

Ответ 1

Просто используйте ClusterizeJS! Он может обрабатывать сотни тысяч строк и был построен именно для этой цели.

Как это работает, спросите вы?

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

Ответ 2

Чтобы иметь возможность обрабатывать большие объемы данных, вам нужна виртуализация данных. Однако у него есть некоторые ограничения.

Сначала вам нужно определить размер порта представления. Предположим, вы хотите отобразить 10 элементов в строке и 20 элементов в столбце. Тогда это будет 10x20 пунктов. В игре вы играете div с оберткой id.

Затем вам нужно знать общий объем данных, которые у вас есть. С вашей скрипки это будет 100x100 предметов. А также вам нужно знать высоту и ширину элемента (ячейки). Возьмем 40x120 (в px).

Итак, div # wrapper - это порт представления, он должен иметь фиксированный размер, например, 10x20 элементов. Затем вам нужно настроить правильную ширину и высоту для таблицы. Высота table будет равна общему количеству данных в столбце, включая высоту по высоте. Ширина для таблицы будет общим количеством элементов в одной строке по ширине элемента.

После того, как вы настроите их, обертка div # получит горизонтальные и вертикальные свитки. Теперь вы можете прокручивать влево и вправо, но это будет просто пустое пространство. Однако это пустое пространство позволяет хранить точный объем данных, которые у вас есть.

Затем вам нужно взять данные прокрутки влево и вверху (положение), которые поступают в пикселях, и нормализовать их до количества элементов, чтобы вы не знали, сколько пикселей вы прокрутили, но сколько прокрученных элементов (или строки, если мы прокручиваем сверху вниз).

Это можно сделать путем разделения пикселей, прокручиваемых по высоте элемента. Например, вы прокручивали влево на 80 пикселей, это 2 элемента. Это означает, что эти предметы должны быть невидимыми, потому что вы прокручивали их. Значит, вы знаете, что вы прокрутили более 2 предметов, и вы знаете, что вы должны увидеть 10 элементов подряд. Это означает, что вы берете свой массив данных, который имеет данные для строки со 100 элементами, и нарезайте его так:

var visibleItems = rowData.slice(itemsScrolled, itemsScrolled + 10);

Это даст вам элементы, которые должны быть видны в окне просмотра в текущей позиции прокрутки. Когда у вас есть эти элементы, вам нужно построить html и добавить его в таблицу.

Также в каждом событии прокрутки вам нужно установить верхнюю и левую позиции для tbody и thead, чтобы они перемещались со свитком, иначе у вас будут ваши данные, но они будут в (0; 0 ) внутри окна просмотра.

В любом случае код говорит тысяча слов, поэтому здесь скрипка: https://jsfiddle.net/Ldfjrg81/9/

Примечание, что этот подход требует точности высот и ширины, иначе он будет работать некорректно. Также, если у вас есть предметы разных размеров, это также следует учитывать, поэтому лучше, если у вас есть фиксированные и равные размеры предметов. В jsfiddle я прокомментировал код, который заставляет первый столбец оставаться на месте, но вы можете отображать его отдельно.

Это хорошее решение для использования в какой-либо библиотеке, как это предлагается в комментариях, поскольку оно обрабатывает много случаев для вас.

Вы можете сделать рендеринг еще быстрее, если использовать response.js или vue.js

Ответ 3

Это не будет ответом, который вы ищете, но здесь мои 2 цента в любом случае.

Javascript-анимация (особенно с учетом того, что DOM должна отображать) никогда не будет столь гладкой, как вы этого хотите. Даже если вы можете получить его гладким на вашем компьютере, скорее всего, он будет сильно отличаться от других людей (старые ПК, браузеры и т.д.).

Я бы увидел 2 варианта, если бы я сам занялся этим.

  • Пойдите в старую школу и добавьте горизонтальную и вертикальную полосу прокрутки. Я знаю, что это не красивое решение, но оно будет работать хорошо.

  • Выводите только определенное количество строк и отбрасывайте их вне экрана. Это может быть немного сложнее, но по существу вы должны сделать 10 строк. Когда пользователь прокручивается до точки, где должен находиться 11-й, отрисуйте его и удалите 1-й. Вы бы вставляли их и при необходимости.

В терминах фактического JS (вы упомянули о том, чтобы вставить элементы в массив), это не поможет. Фактическая изменчивость обусловлена ​​тем, что браузеру необходимо отображать это множество элементов.

Ответ 4

Вы испытываете прерывистую/неглазурованную прокрутку, потому что событие scroll срабатывает при очень высоком темпе.

И каждый раз, когда он срабатывает, вы настраиваете положение многих элементов: это дорого и, кроме того, до тех пор, пока браузер не завершит перерисовку не отвечает (здесь измененная прокрутка).

Я вижу два варианта:

Вариант номер один: отображает только видимое подмножество всего набора данных (это уже было предложено в другом ответе, поэтому я не буду дальше)


Номер опции два (проще)

Во-первых, пусть анимации при изменениях left и top css происходят через переходы. Это более эффективно, не блокирует и часто позволяет браузеру использовать gpu

Затем вместо повторной настройки left и top сделайте это один раз в то время; например, 0,5 секунды. Это выполняется с помощью функции ScrollWorker() (см. Код ниже), которая напоминает себя через setTimeout().

Наконец, используйте обратный вызов, вызванный событием прокрутки, чтобы сохранить обновленную позицию #scroller (сохраненную в переменной).

// Position of the `#scroller` element
// (I used two globals that may pollute the global namespace
// that piece of code is just for explanation purpose)    

var oldPosition, 
    newPosition;


// Use transition to perform animations
// You may set this in the stylesheet

$('th').css( { 'transition': 'left 0.5s, top 0.5s' } );
$('td').css( { 'transition': 'left 0.5s, top 0.5s' } );


// Save the initial position

newPosition = $('#scroller').position();
oldPosition = $('#scroller').position();


// Upon scroll just set the position value

iScroll.on('scroll', function() {
    newPosition = $('#scroller').position();
} );


// Start the scroll worker

ScrollWorker();


function ScrollWorker() { 

    // Adjust the layout if position changed (your original code)

    if( newPosition.left != oldPosition.left || newPosition.top != oldPosition.top ) {
        $('#scroller th:nth-child(1)').css({top: (-newPosition.top), left: (-newPosition.left), position:'relative'});
        $('#scroller th:nth-child(n+1)').css({top: (-newPosition.top), position:'relative'});
        $('#scroller td:nth-child(1)').css({left: (-newPosition.left), position:'relative'});

        // Update the stored position

        oldPosition.left = newPosition.left;
        oldPosition.top  = newPosition.top;

        // Let animation complete then check again
        // You may adjust the timer value
        // The timer value must be higher or equal the transition time

        setTimeout( ScrollWorker, 500 );

    } else {

        // No changes
        // Check again after just 0.1secs

        setTimeout( ScrollWorker, 100 );
    }
}

Вот Fiddle

Я установил Рабочий темп и переход к 0,5 с. Вы можете отрегулировать значение с более высоким или более низким временем, в конечном счете, динамическим способом, основанным на количестве элементов в таблице.

Ответ 5

Да! Вот некоторые улучшения кода из вашего JS Fiddle. Вы можете просматривать мои изменения по адресу: https://jsfiddle.net/briankueck/u63maywa/

Некоторые предлагаемые улучшения:

  • Переключение значений position:relative в слое JS на position:fixed на уровне CSS.
  • Сокращение цепей DOM jQuery, чтобы код не начинался с корневого элемента и проходил весь путь через dom с каждым $lookup. Скроллер теперь является корневым элементом. Все использует .find() для этого элемента, что создает более короткие деревья, и jQuery может быстрее пересекать эти ветки.
  • Перемещение кода регистрации из DOM и в console.log. Я добавил отладочный переключатель, чтобы отключить его, поскольку вы ищете самую быструю прокрутку на столе. Если он работает достаточно быстро для вас, вы всегда можете снова включить его, чтобы увидеть его в JSFiddle. Если вам действительно нужно увидеть это на iPhone, то его можно добавить в DOM. Хотя, вероятно, не обязательно видеть значения слева и верхней позиции в iPhone.
  • Удалите все посторонние значения $, которые не отображаются в объект jQuery. Что-то вроде $scroller запутывается с $, поскольку последняя является библиотекой jQuery, но первая не является.
  • Переход на синтаксис ES6 с помощью let вместо var сделает ваш код более современным.
  • В теге <th> есть новый левый расчет, который вы хотите посмотреть.
  • Слушатель событий iScroll очищен. В position:fixed для верхних тегов <th> необходимо иметь свойство top, применяемое к ним. Левые теги <td> должны иметь свойство left, применяемое к ним. В углу <th> должно быть применено свойство top и left.
  • Удалите все ненужное, например, посторонние теги HTML, которые использовались для ведения журнала.
  • Если вы действительно хотите больше ваниль, измените методы .css() для фактических свойств .style.left= -pos.left + 'px'; и .style.top= -pos.top + 'px'; в JS-коде.

Попробуйте использовать инструмент diff, например WinMerge или Помимо Сравните, чтобы сравнить код с вашей версии с тем, что в моих праведниках, чтобы вы могли легко видеть различия.

Надеемся, что это сделает плавность более гладкой, так как в событии прокрутки нет необходимости обрабатывать все, что ему не нужно... например, 5 полных переходов DOM, а не 3 коротких поисковых запроса,

Наслаждайтесь!:)

HTML:

<body>
<div id="wrapper">
  <table id="scroller">
    <thead>
    </thead>
    <tbody>
    </tbody>
  </table>
 </div>
</body>

CSS

/* ... only the relevant bits ... */

thead th {
  background-color: #99a;
  min-width: 120px;
  height: 32px;
  border: 1px solid #222;
  position: fixed; /* New */
  z-index: 9;
}

thead th:nth-child(1) {/*first cell in the header*/
  border-left: 1px solid #222; /* New: Border fix */
  border-right: 2px solid #222; /* New: Border fix */
  position: fixed; /* New */
  display: block; /*seperates the first cell in the header from the header*/
  background-color: #88b;
  z-index: 10;
}

JS:

// main code
let debug = false;

$(function(){ 
  let scroller = $('#scroller');
  let top = $('<tr/>');
  for (var i = 0; i < 100; i++) {
    let left = (i === 0) ? 0 : 1;
    top.append('<th style="left:' + ((123*i)+left) + 'px;">'+ Math.random().toString(36).substring(7) +'</th>');
  }
  scroller.find('thead').append(top);
  for (let i = 0; i < 100; i++) {
    let row = $('<tr/>');
    for (let j = 0; j < 100; j++) {
      row.append('<td>'+ Math.random().toString(36).substring(7) +'</td>');
    }
    scroller.find('tbody').append(row);
  }

  if (debug) console.log('initialize iscroll');
  let iScroll = null;
  try {
    iScroll = new IScroll('#wrapper', { 
      interactiveScrollbars: true, 
      scrollbars: true, 
      scrollX: true, 
      probeType: 3, 
      useTransition:false, 
      bounce:false
    });
  } catch(e) {
    if (debug) console.error(e.name + ":" + e.message + "\n" + e.stack);
  }

  if (debug) console.log('initialized');

  iScroll.on('scroll', function(){
    let pos = scroller.position();
    if (debug) console.log('pos.left=' + pos.left + ' pos.top=' + pos.top);

    // code to hold first row and first column
    scroller.find('th').css({top:-pos.top}); // Top Row
    scroller.find('th:nth-child(1)').css({left:-pos.left}); // Corner
    scroller.find('td:nth-child(1)').css({left:-pos.left}); // 1st Left Column
  });
});

Ответ 6

Нужно ли создавать собственный скроллер? Почему бы вам просто не форматировать данные в HTML/CSS и просто использовать атрибут overflow? JavaScript нуждается в работе над его возможностью настройки кадров. Раньше я использовал ваш jFiddle, и он отлично работал с обработчиком переполнения.

Ответ 7

Обнаружено это в руководстве. Наверное, не то, что вы хотите услышать, но так оно и есть:

IScroll - это класс, который необходимо инициировать для каждой области прокрутки. Нет ограничений на количество iScrolls, которое вы можете иметь на каждой странице, если это не связано с процессором/памятью устройства. Постарайтесь максимально упростить DOM. iScroll использует слой компоновки оборудования, но есть ограничение на элементы, которые может обрабатывать оборудование.

Ответ 8

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

вызовы обработчика событий прокрутки

Снимок экрана показывает, что произошло, когда я отслеживал, сколько раз обработчик событий запускал, прокручивая всего несколько секунд. Обработчик событий с большим количеством вычислений был запущен более чем в 600 раз!!! Это более 60 раз в секунду!!!

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

Итак, как вы создаете обработчик событий, который срабатывает на максимальной частоте, например 25 раз в секунду, даже если он срабатывает гораздо чаще, например 100 раз в секунду?

Наивный способ сделать это - запустить событие setInterval. Это лучше, но ужасно неэффективно. Существует лучший способ сделать это, установив задержанный обработчик событий и очистив его при последующих вызовах, прежде чем устанавливать его снова, до тех пор, пока не пройдет минимальный интервал времени. Таким образом, он работает не чаще, чем на максимальной желаемой частоте. Это один из основных аргументов в пользу того, почему был изобретен метод `clearInterval '.

Вот действующий рабочий код:

https://jsfiddle.net/pgjvf7pb/7/

Примечание: при постоянном освещении этот столбец заголовка может появиться вне позиции.

Я рекомендую делать обновление только тогда, когда прокрутка приостанавливается примерно на 25 мс или около того, а не непрерывно. Таким образом, пользователю кажется, что столбец заголовка динамически вычисляется, а также фиксируется на месте, потому что он появляется мгновенно после прокрутки, а не для прокрутки данных.

https://jsfiddle.net/5vcqv7nq/2/

Логика такова:

за пределами обработчика событий

  // stores the scrolling operation for a tiny delay to prevent redundancy
  var fresh;

  // stores time of last scrolling refresh
  var lastfresh = new Date();

операции внутри обработчика событий

  // clears redundant scrolling operations before they are applied
  if (fresh) clearTimeout(fresh);

  var x = function() {
    // stores new time of scrolling refresh
    lastfresh = new Date();
    // perform scrolling update operations here...
  };

  // refresh instantly if it is more than 50ms out of date
  if (new Date() - lastfresh > 50) x();

  // otherwise, pause for half of that time to avoid wasted runs
  else fresh = setTimeout(x, 25);

Демо: https://jsfiddle.net/pgjvf7pb/7/

Еще раз, я рекомендую удалить строку кода, которая мгновенно обновляет данные, и условие else после этого, и просто используйте одну строку

 fresh = setTimeout(x, 25);

Появится возможность мгновенного вычисления столбца заголовка в момент завершения прокрутки и сохранения еще больших операций. Моя вторая ссылка на JS Fiddle показывает, как это выглядит, здесь: https://jsfiddle.net/5vcqv7nq/2/