---
title: Про infinite scrolling
date: 2015-01-05T10:00
tags: [javascript]
---

Бесконечный скролл, т.е. постепенный показ и подгрузка данных как реакция на действия пользователя, закрепился в SPA-приложениях. Я работаю с angular, но в целом это может и к backbone относиться..

Технически, данные на frontend хранятся в какой-то коллекции/сервисе. То что **показывается** пользователю — подмножество этих данных, т.е. может быть отфильтрованным, отсортированным и изменяемым на лету. Таких подмножеств в приложении может быть несколько, в зависимости от отображения (view). 

В одном месте у вас _товары_ одного _бренда_, в другом месте у вас _заказанные_ товары, в третьем - товары _конкретного пользователя_.

![](../img/Pasted%20image%2020241016182304.png)

### Что значит «ещё»?

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

Сначала я подумал что лучше всего запрашивать данные **по времени**. У каждого ряда данных будет _timestamp_, как у твиттера, соответсвенно запрос будет высылаться "подгрузи 20 рядов данных, старше X", а время будет браться у последнего ряда. 

Но тут возникает несколько проблем. 

1. Что если у данных одно и то же время? Если на backend добавить вторую сортировку по ID, и передавать ID последнего ряда вместе со временем, то это всё-равно не спасает положения, т.к. значительно усложняется вычисление OFFSET в SQL запросе.
2. Если запрос зависит от последнего _показанного ряда_, то получается что запрос формируется на основе подмножества, т.е. зависит от view. Это плохо, т.к. завязывает модель, контроллер и view в единое целое. Альтернатива - дублировать фильтрацию которая происходит для view в модели.
3. Что если данные обновились со времени последнего запроса? Надо ли их добавлять в начало списка? 
4. Что делать если данные отсортированы не по времени, а по алфавиту? И они вдруг поменялись? 

Вобщем приходишь к тому, что для универсального решения, надо по-прежнему работать с OFFSET и ORDER BY, как то было с постраничной навигацией. При этом offset высылается в зависимости от нужды view и он равен размеру подмножества.На случай обновившихся данных, я использую socket.io, который оповещает что надо обновить. Добавившиеся данные я сразу внедряю в список, а update происходит автоматически

### Когда загружать?

[ngInfiniteScroll](http://binarymuse.github.io/ngInfiniteScroll/index.html) - простенькая ангулярная директива для ангуляра, которая триггерит нашу функцию подгрузки если вы **достаточно проскроллили вниз** (scrollTop() близок к высоте элемента). Проблемы в ней две - она привязана ко всему окну (window). Обычно же у нас может быть несколько контейнеров на странице где надо подгружать данные. Есть и [более простой пример](http://jsfiddle.net/vojtajina/U7Bz9/)

И вторая проблема следующая из неё - если вы запросили слишком мало данных за запрос (LIMIT 5), и/или их высота динамическая и меньше чем высота контейнера, то ваш scroll никогда не появится и не запустит постоянное дополнение списка. Пришлось это [поисправлять](https://github.com/tot-ra/ngInfiniteScroll/blob/master/build/ng-infinite-scroll.js), каждый раз расчитывая высоту содержания контейнера.

Тут главное не заDDOSить сервер из-за неправильного расчёта высоты элементов и рекурсии. По-хорошему вызов подгрузки надо начинать до достижения нижнего края.

### Search

Есть небольшая проблема, если у вас есть фильтрация коллекции с поиском. Я решаю эту проблему отдельным мини-кэшем и отдельными запросами. Если вы ищете что-то по ключевому слову, то результаты не добавляются в основную коллекцию списка, поскольку это повлияет на offset при нормальном режиме. Search и отфильтрованные данные у меня подгружаются без постраничной навигации, т.е. их количество ограничено

### Когда использовать не стоит

Бесконечный скролл напрямую зависит от скролла. Замечали сайты на которых открываешь в телефоне страничку с контактами, а там google map на всю высоту экрана? И не добраться до текста с телефонами ниже.. Бесконечный скролл, если он достаточно высокий, аналогично блокирует данные ниже себя.

Во-вторых, бесконечность дезориентирует и лишает контроля. Вы не знаете сколько вы уже проскроллили, дажа примерно глядя на размер бегунка, не можете понять сколько осталось до конца. Аналогично, вы не можете перепрыгнуть в "значительно более далёкие" версии данных.

![](../img/Pasted%20image%2020241016182340.png)

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

Само собой, если у вас SPA, то могут быть проблемы с индексацией поисковиками. Гугл предлагает [гибридный вариант](http://googlewebmastercentral.blogspot.com/2014/02/infinite-scroll-search-friendly.html), где данные таки будут доступны и по страницам тоже.

Наконец, если у вас что-то случилось с ajax-запросом и нет хорошей обработки ошибок, бесконечный скролл ломает глубину просмотра. А если сервер достаточно нестабильный, то риск что пользователь никогда не сможет добраться до нужных данных увеличивается с каждым запросом. Например если вероятность ответа сервера HTTP 500 оценивается в 5%, то после 15ти последовательных запросов вероятность что лента будет сломана уже (100 - 100 × 0.9515) = 55%.

Тем не менее, если вы делаете крутой динамический сайт, который может обновляется блоками с нестатичными размерами и с нестандартной навигацией вверх/вниз и вправо/влево, постепенная подгрузка musthave.

По теме:

- [Infinite scrolling: Let's get to the bottom of this](http://www.smashingmagazine.com/2013/05/03/infinite-scrolling-lets-get-to-the-bottom-of-this/)
- [Masonry docs - appended](http://masonry.desandro.com/methods.html#appended)   
- [Wordpress infinite scroll](http://jetpack.me/support/infinite-scroll/) 
- [Clusterize.js](http://nexts.github.io/Clusterize.js/)

## Related

- [Backbone.js](/ru/blog/tech/frontend/javascript/backbonejs/)
- [Excel через javascript](/ru/blog/tech/frontend/javascript/excel-cherez-javascript/)
- [JSDoc](/ru/blog/tech/frontend/javascript/jsdoc/)
