Спроектируйте бесконечную ленту (infinite scroll) с тысячами карточек: загрузка данных, виртуализация, сохранение позиции.
Короткий ответ
- Пагинация курсором надёжнее offset при живой ленте
- Подгрузка через IntersectionObserver по элементу-стражу
- Виртуализация: рендерить только видимое окно плюс запас
- Переменная высота карточек — измерение и кэш высот
- Восстановление позиции при возврате к ленте
- Состояния: скелетоны, ошибка страницы с повтором, конец ленты
- Доступность: фокус, «вернуться наверх», усталость от бесконечности
Бесконечная лента держится на курсорной пагинации, дозагрузке через IntersectionObserver и виртуализации списка, а зрелость решения проверяется восстановлением позиции скролла и обработкой ошибок дозагрузки.
Как сказать вслух
пример ответаСначала данные: беру пагинацию по курсору, а не по номеру страницы — в живой ленте элементы добавляются, и офсеты начинают дублировать записи. Подгрузку следующей порции запускаю через IntersectionObserver по невидимому элементу внизу, с запасом до конца списка. Чтобы DOM не распухал на тысячах карточек, применяю виртуализацию: в документе живут только видимые элементы плюс буфер, остальное — пустые отступы. Отдельно продумываю возврат из карточки обратно в ленту с сохранением позиции и честные состояния: скелетоны, ошибку дозагрузки с кнопкой повтора, конец ленты.
Подробный ответ
Основной ответ
Данные: API с курсором (cursor/nextToken) стабилен при вставках в начало ленты; ответ несёт items, nextCursor и признак конца. Дозагрузка: страж-элемент с IntersectionObserver и rootMargin в несколько сотен пикселей, чтобы грузить заранее; обязательна защита от параллельных запросов (флаг isFetching) и ретрай при ошибке — иначе лента молча «обрывается». Виртуализация: окно видимых элементов вычисляется от scrollTop, высота списка имитируется контейнером-распоркой; при переменной высоте карточек используют оценку с дозамером после рендера и кэш измеренных высот (готовые решения — react-window, TanStack Virtual). Изображения — lazy loading с заданными размерами против сдвигов макета (CLS). Возврат к ленте: сохранять позицию и загруженные данные в кэше состояния или sessionStorage, иначе пользователь теряет место — одна из самых раздражающих ошибок лент. UX-зрелость: кнопка «наверх», сохранение доступа к футеру, «Load more» вместо автоскролла там, где важна достижимость конца, живые области для скринридеров.
Ключевые моменты
- Курсор против offset. При вставках в живую ленту offset-пагинация дублирует и пропускает элементы; курсор от последнего элемента стабилен.
- Виртуализация. DOM на десятки тысяч узлов убивает скролл и память; рендер окна с буфером держит список быстрым.
- Восстановление позиции. Переход в карточку и назад должен возвращать к тому же месту — это требует кэша данных и позиции скролла.
- Отказоустойчивость. Ошибка дозагрузки с видимым повтором и защита от дублирующихся запросов отличают продакшен от демо.
Практический контекст
Классическая system-design-задача про производительность DOM и работу с данными. Интервьюер ждёт поэтапного рассуждения: сначала наивное решение, затем узкие места (распухание DOM, гонки подгрузки, потеря позиции) и их лечение. Сильный сигнал — обсуждение границ: когда бесконечная лента вообще неуместна и честная пагинация лучше, например в каталоге с фильтрами и ссылками на страницы.
Частые ошибки
- Предлагают offset-пагинацию, не замечая дублей при живых вставках в ленту
- Рендерят все загруженные карточки в DOM без виртуализации
- Забывают про возврат к позиции скролла и ошибку дозагрузки