Ви випустили список. Він працював у стенді. Потім з’явилися реальні користувачі: хтось втратив місце після натискання «Назад», аналітика різко впала, служба підтримки писала «він завжди підвантажується», а SRE питає, чому один endpoint тепер відповідає за половину читальних IOPS.
Пагінація та нескінченний скрол — це не просто «UX-вибір». Це вибір для розподілених систем під маскою інтерфейсу. Обереш неправильно — постраждають користувачі, база даних, кеш і чергові — часто всі одночасно.
Приймайте рішення як оператор, а не як дизайнер
Пагінація й нескінченний скрол оптимізують різні речі. Якщо обирати за смаком, випадково оптимізуєш під найголоснішого в кімнаті. Обирайте на основі наміру користувача, відновлення після помилок і операційних витрат.
Наміри користувача визначають стандарт
- Користувачі, які шукають щось конкретне (товари, тікети, акаунти, документація): за замовчуванням — пагінація. Люди хочуть орієнтири: номери сторінок, стабільні URL, «Назад», що працює, і відчуття прогресу.
- Користувачі, що переглядають для розваги (стрічки, галереї натхнення, соціальні таймлайни): нескінченний скрол може працювати, бо «далі» менш важливе за «зараз».
- Користувачі, що порівнюють елементи (шопінг, дашборди): пагінація або гібрид із «Завантажити ще» та збереженим станом. Порівняння вимагає повернення в попереднє місце без втрати контексту.
Операційні реалії, які мають впливати на UX
Нескінченний скрол — це не «без сторінок». Це багато маленьких сторінок, що завантажуються послідовно, а це означає:
- Більше мережевих запитів у сесії.
- Більший тиск на пам’ять клієнта, якщо не використовувати віртуалізацію.
- Складніше кешування, якщо API не орієнтований на cursors.
- Складніша атрибуція в аналітиці без попереднього планування.
- Складніша відладка, бо один «скрол» може торкатися кількох сервісів.
Пагінація теж не «вирішена». Вона може бути повільною, неточною й дорогою, коли реалізована як «OFFSET 90000 LIMIT 50» проти «гарячої» таблиці. Коли ви бачите такий шаблон запиту, практично чуєте, як база даних важко зітхає.
Цитата, за якою реально можна керувати сервісом
Надія — не стратегія.
— Генерал Гордон Р. Салліван
Якщо ваш нескінченний скрол спирається на «сподіваймося, користувач не дійде до 10 000 елементів», ви створили часову бомбу зі смугою прокрутки.
Кілька фактів і історія, що досі важливі
- Ранні веб-списки були розбиті на сторінки здебільшого через пропускну здатність: dial-up робив «завантажити все» неможливим, тож межі сторінок були хаком продуктивності до того, як стали UX-патерном.
- Нескінченний скрол поширився наприкінці 2000-х, коли стрічки та соціальні таймлайни оптимізувалися під залучення, а не під завершення завдання.
- Пошукові системи історично мучилися з нескінченним скролом, бо контент без стабільних URL важко обходити та індексувати; «красиво» може бути невидимим.
- Пагінація через offset має алгоритмічну ношу: глибокі зсуви часто вимагають сканування/пропуску рядків, перетворюючи «сторінку 2000» на повільну прогулянку базою даних.
- Cursor-based пагінація стала поширеною в масштабних API, бо вона стабільна при одночасних вставках і видаленнях — «наступна сторінка» менше пересувається.
- Віртуалізовані списки — вирішення проблеми надмірного DOM: рендер тисяч вузлів убиває FPS і батарею; віртуалізація рендерить лише видимі елементи.
- Поведінка кнопки «Назад» — історичний UX-контракт: браузери привчили людей, що «назад» відновлює стан; нескінченний скрол порушує це, якщо історію не керувати уважно.
- HTTP-кешування любить стабільні URL: пагіновані URL добре кешуються; запити «дай більше стрічки після cursor X» теж кешуються, але лише якщо ви так їх спроєктували.
Шаблони, що не дратують користувачів
Шаблон 1: пагінація для списків, орієнтованих на намір
Використовуйте пагінацію, коли користувач цінує позицію і повернення: результати пошуку, адмін-таблиці, логи аудиту, звіти, інвентар. Дайте їм:
- Стабільні параметри в URL (query + page або cursor).
- Видимий прогрес (номери сторінок або «1–50 з 12 430»).
- Елементи керування, що працюють з клавіатурою і скрінрідерами.
- «Перейти на сторінку» тільки якщо це справді потрібно (про це далі).
Шаблон 2: нескінченний скрол для пасивного перегляду
Нескінченний скрол підходить, коли завдання користувача — «продовжувати дивитися». Але не переносіть механіку TikTok за замовчуванням у корпоративний лог аудиту і не називайте це модерном.
Успішний нескінченний скрол має кілька ознак:
- Сильна поведінка «відновити там, де я був» після Натискання Назад/вперед/навігації.
- Явні стани завантаження (і чітке припинення при помилках).
- Віртуалізація, інакше ваш UI перетвориться на підігрівач простору.
- Шлях назовні: доступ до футера, «Назад до верху», «Перейти до фільтрів» і стан кінця списку, якщо це доречно.
Жарт №1: Нескінченний скрол схожий на шведський стіл: приємно, доки не зрозумієш, що виходу немає, а батарея телефону 3%.
Шаблон 3: «Завантажити ще» — заспокійливіший нескінченний скрол
Якщо хочете залучення нескінченного скролу без хаосу — випустіть кнопку Завантажити ще. Вона явніша, простіша для відладки і дружніша до інструментів доступності. Також зупиняє випадкові «штормові» скрол-події, коли тачпад вирішує бути активним.
Шаблон 4: «Пагінація з префетчингом» — швидко без втрати структури
Пагінація не повинна здаватися повільною. Префетчте наступну сторінку, коли користувач перебуває на 70–80% поточної, а потім миттєво замініть контент при кліку. Збережіть межі сторінки для URL і аналітики, але усуньте очікування.
Шаблон 5: «Якірований нескінченний скрол» для коректності історії
Це доросла версія нескінченного скролу: під час прокрутки ви оновлюєте URL, щоб він відображав поточний якір (номер сторінки або cursor) і зберігаєте позицію прокрутки в історії. «Назад» повертає до точної позиції. Це додаткова робота. Але саме так ви уникнете запитів у службу підтримки, що починаються з «Я загубив(лася)».
Правильна пагінація (UI + API)
Правила UI, що запобігають rage-click
- Завжди показуйте, де користувач знаходиться: номер сторінки і діапазон результатів. «Сторінка 7» краще за «ще щось внизу».
- Тримайте розмір сторінки передбачуваним: зміна кількості елементів на сторінці під час сесії порушує ментальну карту.
- Зробіть «Назад» працездатним: зберігайте стан у URL і відновлюйте фільтри/сортування. Якщо щоб повернутися потрібно три кліки — ви побудували лабіринт.
- Не перебільшуйте з лінками сторінок: показуйте вікно (наприклад, 1 … 6 7 8 … 200). Користувачам не потрібен календар вашого датасету.
- Дозволяйте «перейти на сторінку» тільки з запобіжними заходами: стрибки на глибокі сторінки можуть бути дорогими й непослідовними, якщо бекенд не підтримує це ефективно.
Правила API: offset vs cursor і коли що шкодить
Offset-пагінація (page=7, size=50) проста й стабільна для невеликих наборів даних. Вона ламається, коли:
- Ви маєте глибоке пагінування (page 500+).
- Рядки часто вставляються/видаляються; «сторінка 7» зсувається і з’являються дублі.
- Ваш DB-запит стає операцією O(n) пропуску.
Cursor-based пагінація (after=cursor) краще для великих і змінних наборів. Вона вимагає:
- Стабільного ключа сортування (timestamp + tiebreaker ID або монотонний первинний ключ).
- Токена cursor, що кодує «останню побачену» позицію.
- Уважного продумування фільтрів і сортування, щоб cursors залишалися валідними.
Проєктуйте порядок сортування серйозно
Cursor-пагінація стільки ж хороша, як і порядок, за яким ви її робите. «ORDER BY updated_at DESC» звучить логічно, поки не згадаєш, що оновлення відбуваються. Тоді елементи стрибають, а cursors стають ненадійними. Віддавайте перевагу незмінним ключам сортування:
- Час створення для стрічок (якщо концепція — «новизна»).
- Порядок за первинним ключем для адмін-списків (якщо «стабільність» важливіша за зміст).
- Композитні ключі для унікальності (created_at, id), щоб уникнути дублів при однакових мітках часу.
Зробіть межі сторінок кешованими
Пагінація блищить, коли її можна кешувати. Якщо endpoint — «/search?q=…&page=3», це чистий ключ кешу. З cursors ключі кешу теж працюватимуть, але лише якщо cursors стабільні й не є секретами для користувача. Якщо вони персоналізовані, очікуйте нижчий hit-rate кешу і плануйте потужність відповідно.
Правильний нескінченний скрол (без хаосу)
Правило 1: віртуалізуйте список або платіть батареєю й багами
Якщо ви постійно додаєте DOM-вузли, зрештою зламаєте mobile Safari або принаймні зіпсуєте прокрутку до слайдшоу. Віртуалізація означає рендер лише видимих елементів плюс буфер — розмір DOM залишається обмеженим.
Правило 2: контролюйте конкурентність запитів
Нескінченний скрол тригерить фетчі на основі позиції прокрутки. Без лімітів конкурентності ви:
- Надішлете кілька накладаючихся запитів на той самий cursor.
- Отримаєте перегони відповідей і перестановку елементів.
- Нанесете удар по бекенду, коли користувач різко проскролить до низу.
Використовуйте один одночасний запит на сегмент стрічки. Скасовуйте застарілі запити. Дедуплікуйте елементи за ID.
Правило 3: робіть стани помилки термінальними й відновлюваними
Коли запит падає, не крутите індикатор вічно. Показуйте кнопку «Спробувати ще». Логуйте cursor, стан фільтрів і correlation ID, щоб можна було відтворити проблему на сервері. «Щось пішло не так» без контексту — це інженерний еквівалент зневаги.
Правило 4: явно виправляйте семантику історії
«Назад» має повертати до того самого елемента. Це вимагає:
- Збереження позиції прокрутки в стані історії (не тільки в пам’яті).
- Оновлення URL при зміні якірного елемента (номер сторінки або cursor).
- Відновлення елементів з кешу (на клієнті) або швидке повторне фетчення.
Правило 5: забезпечте шлях до футера
Нескінченний скрол часто прибирає футер, а разом з ним навігацію, посилання на підтримку та юридичний текст. Користувачам це все одно потрібно. Дайте спосіб дістатися до низу або забезпечте липкий футер/утилітарну панель.
Жарт №2: Відлагоджувати нескінченний скрол без логів — як гонити котів, тільки коти — HTTP-запити і вони знають, де ви живете.
Гібридні шаблони, які працюють у реальному світі
Гібрид A: нескінченний скрол в межах сторінкової межі
Показуєте «Сторінка 1» з 50 елементами, але завантажуєте їх поступово під час прокрутки, і тримаєте URL і стан як «page=1». Коли користувач доходить до кінця, він натискає «Наступна сторінка». Це зменшує початкове завантаження і зберігає структуру.
Гібрид B: «Завантажити ще» з нумерацією сторінок у URL
Кожне «Завантажити ще» інкрементує внутрішній лічильник сторінок і оновлює URL, щоб відобразити останню завантажену сторінку. «Назад» працює, аналітика може атрибутувати взаємодію до сторінок, а користувач отримує необривну прокрутку.
Гібрид C: двошарова навігація для «перегляд- потім уточнення»
Почніть із нескінченного скролу для відкриття, але коли користувач застосовує фільтри чи сортування — переключіться на пагінацію. Фільтрування змінює намір: люди перестають блукати і починають шукати. Ваш UI має помічати це перемикання.
Продуктивність і зберігання: що ламається першим
Прихована плата за зберігання «ще одна сторінка»
З позиції інженера зі зберігання, нескінченний скрол схильний створювати довгі сесії з багатьма дрібними запитами. Це змінює ваш I/O-профіль:
- Більше read amplification у базі, якщо глибоке пагінування offset-based.
- Більше churn кешу на CDN або зворотному проксі, якщо cursor-токени не кешуються.
- Більший тиск на об’єктне сховище, якщо кожна картка посилається на кілька зображень і ви лейзі-лоадите без правильних заголовків кешування.
Дизайн бекенд-запитів: тихий вбивця
Якщо ваш API використовує «OFFSET … LIMIT …» на великій таблиці, латентність зростає приблизно пропорційно глибині offset. У продакшені це перетворюється на спайки tail latency. Саме tail latency запам’ятовують користувачі.
Cursor-запити зазвичай виглядають як «WHERE (created_at, id) < (last_created_at, last_id) ORDER BY created_at DESC, id DESC LIMIT 50.» Це краще масштабується, ефективно використовує індекси і поводиться при одночасних записах.
Продуктивність на клієнті: пам’ять і час головного потоку
Без віртуалізації браузер тримає кожен відрендерений вузол, зображення, обробники подій і стан розкладки. Пам’ять росте. Збирання сміття стає дорогим. Прокрутка підвисає. CPU частіше прокидається, що вбиває батарею мобільних.
Обмеження частоти: ваш останній рубіж оборони
Баги нескінченного скролу можуть викликати випадкові потоки трафіку. Лімітуйте частоту по користувачу і по IP. Але робіть це розумно: обмеження повинні деградувати поступово («зупинись, повтори») а не падати в жорстку помилку з порожнім екраном.
Спостережність: вимірюйте те, що відчувають користувачі
Якщо ви не можете цього виміряти, ви будете сваритися. Прокинструйте UI і бекенд зі спільним request ID. Потім вимірюйте:
- Час до перших значущих елементів: як швидко з’являється контент?
- Індикатори jank під час скролу: довгі задачі на головному потоці, пропуски кадрів.
- Кількість запитів за сесію: нескінченний скрол часто збільшує виклики; перевірте, чи це виправдано.
- Рівень помилок за cursor/page: помилки можуть концентруватися на глибоких сторінках через неефективні запити.
- Покинення після завантаження: користувачі йдуть через повільність або через нерелевантний контент?
- Успішність навігації «Назад»: як часто повернення відновлює попередню позицію?
Практичний трюк: логувати найглибший досягнутий індекс елемента і останній стабільний якір (сторінка або cursor). Це перетворює «користувачі це ненавидять» у «70% сесій ніколи не доходять далі 40 елементів, припиніть префетчити 200».
Три корпоративні міні-історії з передової
Міні-історія 1: інцидент, спричинений хибним припущенням
Ми отримали в спадок внутрішню адмін-консоль, що показувала події аудиту. Продакт-менеджер захотів «нескінченний скрол як у сучасних додатках», бо пагінація здавалася застарілою. Команда виконала. Воно вийшло з offset-пагінацією під капотом: кожен скрол викликав той самий endpoint з offset і limit.
Припущення було: «ніхто так далеко не скролить». Це було правда для випадкового перегляду. Неправда для розслідування інциденту. Під час розслідування аналітики прокручували назад години, потім дні. OFFSET-запити ставали дедалі глибшими й латентність росла. UI реагував запуском ще запитів, бо поріг скролу продовжував спрацьовувати, поки попередній запит ще в обробці.
База даних зробила те, що бази роблять, коли її просять пропустити гору рядків: вона розігрілася. CPU підскакував. З’явився replication lag. Раптом адмін-консоль була не єдиним постраждалим — інші сервіси на тому самому кластері почали таймаути. Чергові мусили обмежити цей endpoint і відключити нескінченний скрол за feature flag.
Виправлення не було в «додати більше БД». Виправлення — cursor-пагінація з індексом, що відповідає сортуванню, плюс клієнтський шлюз конкурентності (тільки один запит у польоті). Ми також додали фільтр «Перейти до часу», бо слідчі не хочуть скролити через вівторок, щоб дістатися понеділка; їм потрібен часовий фільтр.
Міні-історія 2: оптимізація, що обернулася проти нас
Інша команда спробувала зробити стрічку миттєвою через агресивний префетч: при завантаженні сторінки фетчили сторінки 1, 2, 3 і 4 паралельно. Вони пишалися. Дашборди показували чудову медіану latency для «першого рендеру сторінки», бо перша сторінка поверталася швидко, а решта тихо підвантажувалася в фоні.
Потім мобільні користувачі почали скаржитися на витрату батареї і трафіку. Тим часом кеш шар погіршився: префетч-запити були персоналізовані, cursor-токени специфічні для користувача, і hit-rate кешу впав. Бекенд отримав стрибок запитів за сесію, навіть від користувачів, які відскочили за п’ять секунд.
Справжня причина провалу — координація. Prefetch не поважав наміри користувача. Він припустив, що кожна сесія — глибока. Також створився бурхливий трафік: кожен перегляд сторінки запускав кілька викликів, збільшуючи навантаження у пікові години дуже синхронно.
Виправлення було нудним: префетч лише на одну сторінку вперед, тільки після того, як користувач показав намір (проскролив за поріг), і ніколи не паралельно з початковим рендером. Також ми додали серверні підказки: повертати has_more і рекомендований prefetch_after_ms у відповіді для динамічного налаштування. Стрічка відчувалася так само. Інфраструктура заспокоїлася.
Міні-історія 3: нудна, але правильна практика, що врятувала день
Команда пошуку e‑commerce хотіла нескінченний скрол для підвищення залучення. SRE був скептичним. Компроміс — staged rollout з запобіжними заходами: feature flag, canary-користувачі, суворі error budgets і kill switch, який можна вимкнути без деплойту.
Вони також виконали непоказну роботу: синтетичні тести, що скролять до фіксованої глибини, захоплюють waterfall traces і перевіряють, що «Назад» відновлює попередню позицію. Вони зберегли пагінаційні URL навіть при використанні «Завантажити ще», оновлюючи стан історії під час просування користувача.
Два тижні після запуску зміна залежності в сервісі ресайзу зображень збільшила час відповіді. Новий UI нескінченного скролу це підсилював, бо користувачі завантажували більше зображень за сесію. Але оскільки команда мала метрики «запитів за сесію» і «час до наступного батчу», вони швидко побачили регресію і використали kill switch, щоб повернутися до пагінації, поки сервіс зображень не відновили.
Ніякої драми. Ніякої war room. Нудний план зробив нудні речі — саме те, що потрібно в продакшені.
Плейбук швидкої діагностики
Коли користувачі повідомляють «скрол ламається» або «пагінація повільна», не починайте з довгих дискусій про UI. Знайдіть вузьке місце в три проходи.
Перший: клієнтська підвисає чи мережа/бекенд повільні?
- Перевірте браузерні трасування продуктивності (long tasks, layout thrash, memory growth).
- Перевірте waterfall запитів: виклики повільні чи їх занадто багато?
- Перевірте, чи зображення домінують у часі передачі.
Другий: модель пагінації API конфліктує з моделлю даних?
- Offset з глибоким пагінуванням? Чекайте DB scans і tail latency.
- Cursor-пагінація, але нестабільне сортування? Чекайте дублі/відсутні елементи й незадоволених користувачів.
- Фільтри не включені в cursor? Чекайте «неправильну» наступну сторінку.
Третій: кешування/ліміти роблять щось небажане?
- Hit rate кешу впав після rollout нескінченного скролу? Cursor-токени, ймовірно, некешовані або занадто дрібні.
- 429s стрибати? Фронтенд, можливо, перезавантажує або ретраїть агресивно.
- CDN трафік зріс? Лейзі-лодування зображень могло спричиняти багато унікальних варіантів.
Практичні завдання: команди, вивід і рішення
Ось завдання, які ви можете виконати сьогодні, щоб припинити здогадки. Кожне містить реальну команду, що означає її вивід і яке рішення з нього випливає.
Завдання 1: Перевірте, чи відбуваються глибокі OFFSET-запити
cr0x@server:~$ sudo grep -E "OFFSET [1-9][0-9]{4,}" /var/log/postgresql/postgresql-15-main.log | tail -n 3
2025-12-29 09:10:02 UTC LOG: duration: 812.433 ms statement: SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 50000 LIMIT 50;
2025-12-29 09:10:03 UTC LOG: duration: 944.120 ms statement: SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 60000 LIMIT 50;
2025-12-29 09:10:04 UTC LOG: duration: 1102.009 ms statement: SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 70000 LIMIT 50;
Значення: Ви використовуєте глибоку offset-пагінацію; латентність росте з глибиною.
Рішення: Перейдіть на cursor-based пагінацію або додайте фільтр за часом/механізм стрибка; не «оптимізуйте» через додаткові ретраї.
Завдання 2: Перевірте використання індексів для пагінованого запиту
cr0x@server:~$ psql -d appdb -c "EXPLAIN (ANALYZE, BUFFERS) SELECT id, created_at FROM events ORDER BY created_at DESC OFFSET 50000 LIMIT 50;"
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Limit (cost=28450.12..28450.25 rows=50 width=16) (actual time=801.122..801.140 rows=50 loops=1)
Buffers: shared hit=120 read=980
-> Gather Merge (cost=23650.00..28600.00 rows=120000 width=16) (actual time=620.440..796.300 rows=50050 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=120 read=980
-> Sort (cost=22650.00..22800.00 rows=60000 width=16) (actual time=580.110..590.230 rows=25025 loops=3)
Sort Key: created_at DESC
Sort Method: external merge Disk: 14560kB
-> Seq Scan on events (cost=0.00..12000.00 rows=60000 width=16) (actual time=0.220..220.300 rows=60000 loops=3)
Planning Time: 0.220 ms
Execution Time: 802.010 ms
Значення: Послідовне сканування + сортування + зовнішнє злиття: ви платите за глибоке пагінування дисковою роботою.
Рішення: Додайте індекс, що відповідає сортуванню, і перейдіть на cursor-пагінацію; якщо offset потрібно залишити тимчасово — обмежте максимальну глибину сторінки.
Завдання 3: Виявити скарги на дублікати елементів, корелюючи cursor-токени
cr0x@server:~$ sudo grep "cursor=" /var/log/nginx/access.log | awk '{print $7}' | tail -n 5
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
/feed?limit=50&cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6IjIwMjUtMTItMjlUMDk6MDk6MDAuMDAwWiJ9
Значення: Той самий cursor повторюється: клієнт перезапитує ту саму сторінку (ймовірно цикл retry або баг конкурентності).
Рішення: Додайте на клієнті дедуплікатор запитів у польоті, backoff при ретраї, і серверну ідемпотентність/дедуплікацію за request ID.
Завдання 4: Перевірити обмеження частоти й чи спрацьовує нескінченний скрол
cr0x@server:~$ awk '$9==429 {count++} END{print "429s:", count}' /var/log/nginx/access.log
429s: 384
Значення: Користувачів обмежують по частоті. Часто це самозапуск через надмірні фетчі + ретраї.
Рішення: Налаштуйте пороги на фронтенді й ретраї; додайте серверні підказки (retry-after) і переконайтесь, що ліміти прив’язані до користувача, а не глобально.
Завдання 5: Перевірити, чи відповіді кешуються
cr0x@server:~$ curl -sI "http://app.internal/search?q=router&page=2" | egrep -i "cache-control|etag|vary"
Cache-Control: public, max-age=60
ETag: "9a1d-17c2f2c"
Vary: Accept-Encoding
Значення: Добре: кешована відповідь з ETag; пагіновані URL ймовірно добре кешуються.
Рішення: Тримайте стабільні URL пагінації; для нескінченного скролу/курсорів подумайте про дружні до кешу токени і короткі TTL.
Завдання 6: Виявити персоналізовані cursor-токени, що вбивають hit rate кешу
cr0x@server:~$ curl -sI "http://app.internal/feed?limit=50&cursor=abc" | egrep -i "cache-control|vary|set-cookie"
Cache-Control: private, no-store
Vary: Authorization
Set-Cookie: session=...
Значення: Відповідь явно некешована і залежить від Authorization.
Рішення: Прийміть витрати (план місткості) або перерахуйте: відокремте персоналізовані дані від публічного вмісту карток; кешуйте те, що можна.
Завдання 7: Знайти UI-індуковані штормові запити (requests per minute)
cr0x@server:~$ sudo awk '{print $4}' /var/log/nginx/access.log | cut -d: -f1,2 | sort | uniq -c | tail -n 5
812 [29/Dec/2025:09:09
945 [29/Dec/2025:09:10
990 [29/Dec/2025:09:11
1044 [29/Dec/2025:09:12
1202 [29/Dec/2025:09:13
Значення: Трафік швидко зростає. Якщо це збігається з релізом фронтенду — підозрюйте тригери/префетч нескінченного скролу.
Рішення: Відкотіть або вимкніть фічу; потім виправте пороги і ліміти конкурентності.
Завдання 8: Підтвердити зростання пам’яті клієнта через RSS процесу node (SSR або BFF)
cr0x@server:~$ ps -o pid,rss,cmd -C node | head -n 5
PID RSS CMD
3221 485000 node server.js
3380 512300 node server.js
Значення: RSS велике й зростає; можливо, SSR зберігає занадто багато стану списку на сесію.
Рішення: Припиніть зберігати стан списку на сесію серверно; кешуйте шаблони або фрагменти, а не користувацьку історію прокрутки.
Завдання 9: Заміряти tail latency API на edge (p95/p99 проксі-статистика)
cr0x@server:~$ sudo awk '$7 ~ /^\/feed/ {print $NF}' /var/log/nginx/access.log | tail -n 5
rt=0.112
rt=0.984
rt=1.203
rt=0.221
rt=1.544
Значення: Часи відповіді сильно варіюються; p99 буде виглядати як «додаток зламався», навіть якщо медіана в порядку.
Рішення: Виправте форму бекенд-запитів; додайте таймаути + fallback UI; зменшіть payload на запит.
Завдання 10: Підтвердити диск I/O під навантаженням глибокої пагінації
cr0x@server:~$ iostat -xm 1 3
Linux 6.5.0 (db01) 12/29/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.40 0.00 6.10 9.80 0.00 71.70
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await aqu-sz %util
nvme0n1 520.0 41280.0 0.0 0.00 18.20 79.38 40.0 2048.0 3.10 9.55 88.00
Значення: Високий read I/O і велике завантаження диска; глибока пагінація може змушувати дискові читання і сортування.
Рішення: Виправте індекси і план запиту; додайте кешування; розгляньте read replicas тільки після виправлення форми запиту.
Завдання 11: Перевірити CDN/об’єктне кешування зображень у списках
cr0x@server:~$ curl -sI "http://cdn.internal/images/item123?w=640" | egrep -i "cache-control|age|etag"
Cache-Control: public, max-age=31536000, immutable
ETag: "img-7c21"
Age: 18422
Значення: Добре кешування. Нескінченний скрол все ще завантажуватиме багато зображень, але повторні перегляди не завантажуватимуть їх знову.
Рішення: Тримайте URL зображень стабільними й незмінними; уникайте генерації унікальних URL для кожного запиту.
Завдання 12: Виявити помилки «нескінченний спіннер» у логах клієнта, що відправляються на сервер
cr0x@server:~$ sudo grep -E "feed_load_failed|pagination_fetch_error" /var/log/app/client-events.log | tail -n 5
2025-12-29T09:11:22Z feed_load_failed cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6Ii4uLiJ9 status=504
2025-12-29T09:11:25Z feed_load_failed cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6Ii4uLiJ9 status=504
2025-12-29T09:11:28Z feed_load_failed cursor=eyJsYXN0X2lkIjoxMjM0NTYsImxhc3RfY3JlYXRlZF9hdCI6Ii4uLiJ9 status=504
Значення: Повторювані 504 для того самого cursor: таймаут бекенду плюс цикл ретраїв на клієнті.
Рішення: Додайте експоненційний backoff і видимий для користувача «Спробувати ще»; виправте причину бекенд-таймауту перед тим, як підвищувати таймаути.
Завдання 13: Підтвердити, що API повертає стабільні ключі сортування для cursors
cr0x@server:~$ curl -s "http://app.internal/feed?limit=3" | jq '.items[] | {id, created_at}'
{
"id": 981223,
"created_at": "2025-12-29T09:13:01.002Z"
}
{
"id": 981222,
"created_at": "2025-12-29T09:13:00.991Z"
}
{
"id": 981221,
"created_at": "2025-12-29T09:13:00.990Z"
}
Значення: Список показує стабільні ключі; можна побудувати cursor на (created_at, id).
Рішення: Використовуйте композитний cursor; уникайте сортування за змінними полями на кшталт updated_at для базової пагінації.
Завдання 14: Перевірити, чи існують HTML-якорі історії для SEO і навігації «Назад»
cr0x@server:~$ curl -s "http://app.internal/search?q=router&page=2" | grep -Eo 'rel="(next|prev)"' | sort | uniq -c
1 rel="next"
1 rel="prev"
Значення: Сторінка декларує next/prev відношення. Це допомагає краулерам і також прояснює структуру навігації.
Рішення: Тримайте це для пагінованих списків; для нескінченного скролу експонуйте еквівалентні пагіновані URL під капотом.
Поширені помилки: симптом → причина → виправлення
1) «Кнопка Назад повертає мене вгору»
Симптом: Користувач клікає на елемент, повертається і втрачає своє місце.
Причина: Позиція прокрутки не збережена; URL не оновлено якірним елементом; стан списку викинуто.
Виправлення: Зберігайте позицію прокрутки в стані історії; оновлюйте URL поточним page/cursor якіром; відновлюйте з кешованих елементів або швидко повторно фетчте.
2) «Я бачу дублі / пропущені елементи під час скролу»
Симптом: Елементи повторюються або з’являються пропуски після завантаження додаткового контенту.
Причина: Нестабільне сортування (updated_at), cursor не прив’язаний до унікального ключа порядку, конкурентні запити з перегонами або відсутня дедуплікація.
Виправлення: Використовуйте незмінне сортування (created_at + id); забезпечте один запит у польоті; дедуплікуйте за ID на клієнті й сервері.
3) «Він завантажується вічно» (нескінченний спіннер)
Симптом: Індикатор завантаження крутиться; нічого нового не з’являється; користувач продовжує скролити.
Причина: Цикл ретраїв на 5xx/таймаути; стан помилки не показано; тригер скролу продовжує спрацьовувати.
Виправлення: Зробіть помилки термінальними з «Спробувати ще»; додайте експоненційний backoff; впровадьте circuit breaker; логуйтесь cursor і request ID.
4) «Сторінка 1 швидка, сторінка 200 — непридатна»
Симптом: Глибинна навігація повільна; p99 вибухає.
Причина: Offset-пагінація сканує великі ділянки; відсутні композитні індекси.
Виправлення: Cursor-пагінація; додати відповідний індекс; обмежити доступ до глибоких сторінок; запропонувати часовий фільтр/стрибок.
5) «Прокрутка підвисає на мобайлі»
Симптом: Низький FPS, затримка відповіді на дотики, пристрій нагрівається.
Причина: Забагато DOM-вузлів, важкі зображення, thrash розкладки, синхронна робота під час скролу.
Виправлення: Віртуалізуйте; використовуйте плейсхолдери зображень і коректні розміри; уникайте синхронної роботи в обробниках скролу; тротуйте observers.
6) «Аналітика нісенітниця після переходу на нескінченний скрол»
Симптом: Конверсії несподівано падають (або стрибають); атрибуція ламається.
Причина: Трекінг, заснований на перегляді сторінок, не підходить для нескінченного скролу; відсутні події для «переглянуто елементів» і «досягнуто глибини».
Виправлення: Трекуйте події експозиції (рендер/видимість елементів), досягнуту глибину і зміну якорів; тримайте URL оновленим для збереження семантики.
7) «Hit rate кешу впав як обрив»
Симптом: Хітрейт CDN/реверс-проксі падає після rollout.
Причина: Персоналізовані cursors, варіація Authorization, заголовки private/no-store.
Виправлення: Розділіть публічні й приватні дані; кешуйте payload карток окремо; тримайте cursor-токени детермінованими; використовуйте короткі TTL там, де безпечно.
8) «Користувачі не дістаються до футера»
Симптом: Підтримка: «Не можу знайти контакт/юридичні/налаштування».
Причина: Нескінченний скрол прибрав природний кінець сторінки.
Виправлення: Забезпечте липку утилітарну панель, можливість «пауза завантаження» або кнопку «Перейти до футера».
Чеклісти / покроковий план
Покроково: вибір шаблону
- Класифікуйте намір: пошук/порівняння (пагінація) vs перегляд (нескінченний або load-more).
- Визначте «повернення до місця»: це вимога? Якщо так — спроєктуйте якір історії з самого початку.
- Виберіть модель API: cursor-based для великих/динамічних наборів; offset тільки для маленьких, стабільних списків.
- Вирішіть стабільні ключі сортування: незмінні ключі з tiebreaker.
- Встановіть бюджет продуктивності: час до наступного батчу, макс. запитів за сесію, макс. DOM-вузлів.
- Сплануйте кешування: що може бути публічним, що має лишатися приватним, і де TTL доречні.
- Прокинструйте семантику аналітики: експозиція, глибина, зміни якорів, поведінка ретраїв.
- Розгортайте з запобіжними заходами: feature flag, canary і kill switch.
Чекліст: UI пагінації, що не дратує
- URL відображає стан (фільтри/сортування/сторінка/cursor).
- Показує загальну кількість результатів або корисну оцінку (і вказує чесно).
- Керування доступне з клавіатури, управління фокусом і ARIA-лейбли.
- Next/prev плюс вікно сторінок; ніяких 200-посилань.
- Префетч наступної сторінки лише коли це не створює піковий трафік.
- Доступ до глибоких сторінок або підтриманий ефективно, або свідомо обмежений.
Чекліст: нескінченний скрол, що не плавить пристрої
- Віртуалізація увімкнена; кількість DOM-вузлів обмежена.
- Один запит у польоті; скасування застарілих викликів; дедуплікація елементів.
- Чіткі стани помилок з Retry; ніяких вічних спіннерів.
- Історія + оновлення URL-якоря; «Назад» повертає на те саме місце.
- Доступ до футера/навігації через постійний UI-елемент.
- Бюджет запитів дотримано (макс. глибина, макс. префетч, макс. одночасні медіа-завантаження).
Чекліст: бекенд-вимоги для обох шаблонів
- Індекс відповідає порядку сортування.
- Cursor-токени включають весь необхідний контекст сортування/фільтрів або безпечно відхиляються.
- Відповідь містить
has_moreі вказівник на наступний cursor/page. - Лімітування та ретраї координовані (429 з Retry-After семантикою).
- Спостережність: request IDs, cursor/page у логах і латентність за процентилями.
Часті питання
1) Чи завжди на мобайлі краще нескінченний скрол?
Ні. Мобільні користувачі мають менше терпіння до повільних завантажень і менше пам’яті для великих DOM. Якщо завдання — пошук/порівняння, пагінація (або «завантажити ще») часто краща.
2) Чи «Завантажити ще» — просто лінива пагінація?
Це пагінація з більш дружнім інтерфейсом. Вона явніша, простіша для доступності і простіша для відладки. Для багатьох продуктів це хороша золота середина.
3) Чому offset-пагінація сповільнюється при великих номерах сторінок?
Бо база часто мусить сканувати/пропустити багато рядків, щоб дістатися offset, а потім сортувати або фільтрувати. Навіть з індексами глибокі offset-и можуть спричиняти роботу, пропорційну відстані пропуску.
4) Чи cursor-пагінація повністю усуне дублікати?
Вона вирішує багато причин, але не всі. Потрібні стабільні ключі сортування і tiebreaker. І все одно потрібна клієнтська дедуплікація, якщо можна відправляти дубль-запити.
5) Як зробити нескінченний скрол дружнім до SEO?
Експонуйте пагіновані URL, що репрезентують ті самі зрізи контенту, і зробіть їх доступними (серверний рендер або принаймні доступні для обходу). Нескінченний скрол може бути клієнтським досвідом; структура для краулінгу має існувати.
6) Чи користувачі надають перевагу нескінченному скролу?
Користувачі надають перевагу тому, що допомагає їм завершити завдання з мінімальним тертям. Для перегляду нескінченний може відчуватися плавно. Для пошуку, порівняння і повернення — зазвичай перемагає пагінація.
7) Який найпростіший спосіб запобігти піковим сплескам трафіку від нескінченного скролу?
Дотримуйтеся одного запиту у польоті, префетчьте максимум одну сторінку вперед, і вимагаєте намір користувача (поріг прокрутки) перед префетчем. Додайте backoff і обмежте ретраї.
8) Чи показувати загальну кількість результатів?
Якщо користувачі приймають рішення на основі обсягу («тільки 23 результати» vs «12 000 результатів»), то так. Якщо рахунок дорогий у підрахунку — покажіть приблизну оцінку або діапазони; не брешіть.
9) Чи можна зберегти номери сторінок з cursor-пагінацією?
Можна, але це складно. Cursor-пагінація не природно дозволяє довільні стрибки сторінок. Якщо потрібні номери сторінок, розгляньте зберігання cursors для кожної сторінки в сесії клієнта або надайте часові стрибки натомість.
10) Що найкраще за замовчуванням для корпоративних адмін-таблиць?
Пагінація з серверним сортуванням і фільтрацією, та стабільні URL. Додавайте «завантажити ще» тільки якщо гарантуєте поведінку «Назад» і продуктивність при глибокому використанні.
Висновок: наступні кроки, що не згасять ваш квартал
Якщо ваш список — інструмент, випускайте пагінацію (або «завантажити ще») зі стабільними URL і cursor-based API. Якщо ваш список — розвага, нескінченний скрол може бути доречним — але тільки з віртуалізацією, контролем конкурентності і реальною семантикою історії.
Наступні кроки, що швидко окупаються:
- Аудит бекенд-запитів на предмет глибокого використання
OFFSETі виправлення форми запиту до змін UI. - Визначте й задокументуйте ключ сортування для пагінації і зробіть його незмінним з tiebreaker.
- Додайте модель якірів (page/cursor) в URL і стан історії, щоб «Назад» працював, як очікують користувачі.
- Прокинструйте глибину, експозицію, ретраї і tail latency — потім встановіть бюджет запитів за сесію.
- Розгортайте з kill switch. Ви будете вдячні собі пізніше, зазвичай о 2:13 ранку.