Фронтенд: шаблон UI пошуку, який робить документацію «миттєвою»

Було корисно?

Пошук по документації — це місце, куди користувачі йдуть, коли навігація підводить, пам’ять підводить або набір документів просто занадто великий. Це також місце, де вмирає довіра до продукту, коли інтерфейс гальмує, нічого не повертає або «шукає», крутячись у лоадері 900 мс, ніби читає чайні листи.

Трюк не в магічній бібліотеці. Це патерн інтерфейсу: відвантажте компактний, кешований індекс ранньо, шукайте спочатку локально, рендерте прогресивно й лише потім звертайтесь у мережу за довгим хвостом. Якщо зробити правильно, це відчувається миттєво, тому що в типовому випадку воно й справді миттєве.

Патерн: «спочатку локально, потім мережа»

Ви хочете, щоб пошук по документації відчувався так, ніби браузер завершує думку користувача, а не веде перемовини з далекою базою даних. UI-патерн, який надійно дає таке відчуття, виглядає так:

  1. Передзавантажте компактний пошуковий індекс якомога раніше (або щонайменше прогрійте його при першій взаємодії).
  2. Шукайте локально на кожному натисканні клавіші алгоритмом, який швидкий і передбачуваний.
  3. Рендерте результати прогресивно (топ N зараз; уточнення/розширення пізніше) зі стабільним макетом.
  4. Використовуйте мережу тільки для довгого хвоста: повні фрагменти контенту, «мабуть ви мали на увазі», аналітика, персоналізоване ранжування або результати по всьому сайту.
  5. Агресивно кешуйте (HTTP-кеш, Service Worker, IndexedDB), щоб «миттєве» залишалось миттєвим при повторних візитах.

Цей патерн однозначно визначає, куди витрачається ваш час. Ви витрачаєте обчислення один раз (під час збірки), щоб створити індекс, який робить виконання в рантаймі дешевим. Ви витрачаєте пропускну здатність один раз (кешовану), тож наступні пошуки — це фактично локальні виклики функцій. Ви уникаєте важкої роботи на головному потоці під час введення користувачем.

Чому «локально-перший» працює саме для документації

Запити в документації зазвичай короткі, неоднозначні й часто виправляються в процесі введення («s3 policy» → «s3 bucket policy deny public»). Користувачі набирають, паузують, набирають знову. Якщо кожне натискання тригерить запит, ваш UI буде коливатись між «завантаження» і «застаріло», а бекенд фактично стане логером натискань клавіш.

Локальний пошук перетворює цю мішанину на детермінований цикл:

  • Введення змінюється
  • Локальний запит виконується за кілька мілісекунд
  • UI рендерить топ-результати
  • Опційно: фонове уточнення або доповнення з мережі

Одна безкомпромісна порада: не вдавайте, що це миттєво, показуючи фальшиві результати. Користувачі відчують підміну. Дайте справжні локальні результати швидко, а потім їх збагачуйте.

Що насправді означає «миттєво» (бюджети затримки, а не відчуття)

«Миттєво» — це бюджет. Це різниця між натисканням клавіші і появою значущих пікселів. На практиці ви балансуєте між:

  • Від введення до наступного малюнка (територія INP): якщо ви блокуєте головний потік, клавіатура відчувається «м’якою».
  • Час до першого результату (TTFR): коли з’являються перші правдоподібні результати.
  • Час до стабільних результатів: коли список перестає стрибати і користувач може впевнено клацнути.

Реалістична мета для документації на середньому ноутбуку й пристойному телефоні:

  • TTFR < 100ms для кешованого локального індексу (швидкий шлях)
  • TTFR < 300ms для першого завантаження індексу з передзавантаженням (прогрітий шлях)
  • Стабільні результати < 500ms навіть коли додаєте фрагменти або серверне перенарахування (шлях збагачення)

UI має поводитися так, ніби він миттєвий: без деренчання, без мерехтіння, без зникнення результатів. Але він має бути чесним: якщо індекс ще не завантажено, показуйте невеликий підказку «Прогрів пошуку» і зберігайте введення чутливим.

Одна цитата, щоб залишатися в реаліях: Надія — не стратегія. — Джеймс Кемерон. Це не про пошук конкретно, але застосовується до надійності та продуктивності щоразу.

Цікаві факти й трохи історії

Трохи контексту полегшує захист патерну на дизайнових оглядах і бюджетних зустрічах.

  1. Typeahead з’явився ще до сучасного фронтенду. Ранні «інкрементальні пошуки» з’являлися в десктопних застосунках десятиліття тому, бо люди не люблять чекати між думкою і відгуком UI.
  2. Ранні UX-дослідження Google популяризували «швидкість як функцію». Основний урок був не просто в швидших серверах; а в усуненні сприйманої затримки миттєвим фідбеком.
  3. CDN зробили статичні доки типовою архітектурою. Коли документація стала статичною + кешованою, природним стало й відвантаження індексів тим же шляхом.
  4. Service Worker (мейнстрім з ~2015) зробив «offline-first» реалістичним, що зручно мапиться на «локально-перший пошук».
  5. Обертальні (inverted) індекси старі. Базова ідея — термін → список документів — використовувалась в інформаційному пошуку задовго до появи вашого сайту і досі є основою швидкого пошуку.
  6. Компресія — це фічa UX. Техніки на кшталт Brotli і словникової компресії — не лише економія трафіку; вони прямо зменшують час до першого результату при холодних завантаженнях.
  7. Мобільні процесори карають неохайний JS. Алгоритм, що відчувається прийнятно на MacBook, може підвісити середній Android, перетворивши «миттєво» на «я краще користуюсь Google».
  8. «Пошук у доках» став базовою вимогою, коли набори документів розрослися. Мікросервіси, SDK і хмарні продукти створили корпорації документів, придатні для навігації лише через пошук.

Референсна архітектура (нудна версія, яка працює)

Час збірки: створіть пакет пошуку, призначений для рантайму

Під час збірки у вас є час і CPU. Використайте їх. Створіть артефакт пошуку, окремий від HTML-сторінок:

  • Файл індексу: терміни + posting-листи або структури, специфічні для бібліотеки.
  • Мапа документів: id документа → URL, заголовок, підзаголовки, опційне резюме.
  • Метадані версії: хеш збірки, версія схеми, мова.

Обмеження дизайну:

  • Індекс має бути достатньо малим для передзавантаження без докорів сумління. Якщо він гігантський — розділіть за секцією, мовою або версією.
  • Індекс має бути довго кешованим (неміняні імена файлів, URL з хешем вмісту). Це дозволяє агресивне кешування без ризику отруєння кешу.
  • Парсинг індексу має бути швидким і інкрементним. Розгляньте бінарний формат або принаймні JSON, оптимізований для швидкого парсингу.

Рантайм: завантажте один раз, виконуйте запити швидко, рендерте прогресивно

У рантаймі UX-цикл має виглядати так:

  1. Передзавантаження в простій момент: після першого рендера контенту, передзавантажте індекс з низьким пріоритетом.
  2. Фолбек при першій взаємодії: якщо користувач фокусує поле пошуку до завершення передзавантаження, підніміть пріоритет і покажіть «Прогрів пошуку».
  3. Локальний запит: запускайте запит у Web Worker коли можливо; якщо ні — тримайте його в межах суворого бюджетного часу.
  4. Рендер топ-хітів: спочатку показуйте заголовки й хлібні крихти (дешево), відкладаючи фрагменти (дорогі).
  5. Збагачення: підвантажуйте фрагменти або запускайте перенарахування асинхронно; оновлюйте UI без надмірного перетасування списку.

Потік даних: двохрівневі результати

Думайте в термінах рівнів:

  • Рівень 1 (локальний): швидкий, приблизний, достатній для ~80% запитів.
  • Рівень 2 (мережа): повільніший, багатший, коректний для крайніх випадків (опечатки, синоніми, фільтрація безпеки, персоналізація).

Коли повертається рівень 2, мерджте результати обережно. Якщо ви переставляєте все після кожної мережної відповіді, UI здається «привидним».

Жарт #1: Найшвидший пошук — той, що не лізе до бекенду; ваша база даних теж хотіла б перестати слухати кожну опечатку.

Деталі UI, що змінюють усе

1) Не блокуй введення

Якщо ваш обробник пошуку виконує важку роботу на кожному натисканні клавіші, інпут лагає. Користувачі звинувачують «пошук», але справжня проблема — конкуренція головного потоку.

Використовуйте:

  • Дебаунс (наприклад, 50–120ms) для дорогих операцій на кшталт генерації фрагментів.
  • Миттєве локальне відсівання для дешевих операцій, як префіксний пошук по заголовках.
  • Web Worker для повного запиту, якщо індекс нетривіальний.

2) Тримайте макет стабільним

Результати пошуку, що сильно змінюють верстку, — мовчазний вбивця конверсій. Якщо список змінює висоту, користувачі часто помилково клікають і потім звинувачують вашу документацію в «заплутаності». Виправте це:

  • Фіксована висота рядків там, де можливо
  • Скелетони тільки коли потрібно (і ніколи як заміна відсутнім результатам)
  • Зарезервувати місце для фрагментів, щоб вони не штовхали все вниз

3) Показуйте «нуль результатів» лише коли впевнені

У патерні локально-перший «немає результатів» може означати «індекс ще не завантажений», «індекс застарів» або «запит надто короткий». Розрізняйте:

  • Завантаження індексу: показуйте «Прогрів пошуку…»
  • Запит надто короткий: показуйте «Введіть щонайменше 2 символи» (або ваш поріг)
  • Справжнє нуль: показуйте підказки (фільтри, орфографія) і, можливо, мережний фолбек

4) Клавіатурна навігація — не опціональна

Користувачі документації живуть клавіатурою. Ваш UI пошуку має підтримувати:

  • Шорткат фокусу (наприклад, / або Cmd+K)
  • Навігацію стрілками
  • Enter для відкриття
  • Escape для закриття

Також: не затягуйте фокус, наче це привидний модальний діалог. Зробіть доступним і передбачуваним.

5) Бути явним щодо сфери пошуку

У доках часто є версії, продукти, мови та права доступу. Якщо сфера пошуку неочевидна, результати виглядають «неправильними». Додайте індикатор сфери: «Пошук: API v2 • Англійська». Так, це займає місце. Займайте його.

Релевантність: ваш індекс — це продукт

Швидкий пошук, який помиляється, — це просто швидкий спосіб втратити довіру.

Що індексувати (і чого уникати)

Індексувати:

  • Заголовок сторінки
  • Підзаголовки (H2/H3)
  • Короткий підсумок/опис (ручний або згенерований під час збірки)
  • Токени шляху/хлібні крихти (продукт, секція)
  • Опційно: кодові символи (імена функцій, прапорці)

Уникайте індексування всього тіла сторінки для локального пошуку, якщо ви не можете робити це ефективно. Повнотекстові тіла роздмухують індекс, збільшують вартість парсингу і сповільнюють запити. Якщо потрібні збіги в тілі — розгляньте другий рівень: локально знаходяться кандидати, мережа повертає фрагменти.

Евристики ранжування, що працюють для доків

  • Підсилення полів: заголовок > підзаголовки > резюме > тіло
  • Свіжість: якщо доки часто змінюються, новіші сторінки можуть отримувати невеликий буст (але не ховайте канонічні старі доки)
  • Буст секції: «документація API» vs «блог» vs «гайди»
  • Точний збіг перемагає: точний збіг заголовка має стрибнути догори
  • Префіксний збіг для символів: «kubectl get» має поводитись як командна палітра

Опечатки й синоніми: обирайте поле бою

Толерантність до опечаток дорога. Виконуйте її локально лише якщо можете її обмежити (наприклад, обмеження редагувальної відстані, малий індекс, worker). Інакше зробіть це функцією другого рівня.

Синоніми — політичне питання. «VM» vs «instance» vs «node» залежить від внутрішньої термінології компанії. Якщо додаєте синоніми, робіть це цілеспрямовано і вимірюйте вплив на CTR і частоту повернення до пошуку.

Інженерія продуктивності: від натискання клавіші до пікселів

Затримка — це властивість кінцевого шляху

Найповільніший компонент перемагає. Типові підозрювані:

  • Завантаження індексу (занадто великий, погане кешування)
  • Час парсингу індексу (величезний JSON + парсинг на головному потоці)
  • Час запиту (поганий алгоритм, надмірна нечіткість)
  • Час рендеру (великий DOM, переробки, дороге підсвічування)
  • Мережне збагачення (повільний edge, latency до origin)

Зробіть швидкий шлях нудним

Відчуття «миттєвості» приходить від передбачуваності. Хороша реалізація має швидкий шлях, який:

  • Кешований: індекс завантажується з кешу більшість часу
  • Виконується у воркері: запит не блокує введення
  • З фіксованою вартістю: лише топ N результатів, фіксований максимум роботи на натискання клавіші

Використовуйте воркер або прийміть свою долю

Якщо ваш індекс більший за демонстраційний набір, вам потрібен Web Worker. Це не «передчасна оптимізація». Це контроль ризику. Затримки головного потоку складно дебажити і легко відправити в продакшен.

Підсвічування: прихований податок

Підсвічування результатів (жирний текст для знайдених підрядків) здається дешевим, поки ви не почнете робити це для 30 результатів, кожного з кількох полів, на кожному натисканні. Обмежте:

  • Підсвічуйте лише видимі рядки
  • Підсвічуйте лише заголовок + одну рядок фрагмента
  • Пропускайте підсвічування, поки користувач швидко друкує (короткий дебаунс)

Жарт #2: Нечіткий (fuzzy) пошук схожий на декофеїновану каву — заспокоює, але якщо перестаратися, нічого не робиться.

Спостережуваність: інструментуйте, як продакшен-сервіс

Пошук у доках — це фронтендна фіча, але вона поводиться як розподілена система: кеш, CDN, планування браузера, мережа, origin і іноді сторонні пошукові API. Ставтеся до цього відповідно.

Що вимірювати

  • Час завантаження індексу: завантаження + парсинг + готовність до запитів
  • Коефіцієнт попадань у кеш: чи індекс був відданий з пам’яті/Cache Storage/HTTP-кеша?
  • TTFR: час від події введення до першого рендера результатів
  • Тривалість запиту: час виконання у воркері на запит
  • Блокування головного потоку: довгі таски під час активного набору
  • CTR та rate повернення до пошуку (проксі для релевантності)
  • Частка нульових результатів (і чи був індекс завантажений)
  • Затримка збагачення та показник помилок

Корелюйте клієнтські метрики з метриками доставки

Коли пошук «відчувається повільним», корінь проблеми може бути в тому, що запит індексу віддався з далекого POP, або заголовки кешу некоректні, або файл індексу збільшився після реорганізації доків. Прив’яжіть фронтенд-метрики до:

  • Заголовків статусу кеша CDN
  • Розміру артефакту індексу і коефіцієнта стиснення по релізу
  • Часу деплоя та інвалідацій

Якщо ви не можете відповісти на «чи був індекс у кеші у користувача?», ви будете сперечатися у тумані.

Практичні завдання з командами, виводами та рішеннями

Ось перевірки, які я виконую, коли хтось каже: «Пошук повільний», і мій пейджер починає косо дивитися. Кожне завдання містить: команду, що означає вивід, і рішення, яке з цього випливає.

Завдання 1: Підтвердити розмір файлу індексу і компресію на диску

cr0x@server:~$ ls -lh public/search/index.json public/search/index.json.br
-rw-r--r-- 1 deploy deploy 18M Jan 12 10:14 public/search/index.json
-rw-r--r-- 1 deploy deploy 3.2M Jan 12 10:14 public/search/index.json.br

Значення: Сирий JSON — 18MB; Brotli зменшує до 3.2MB. Це передзавантажувано на широкому каналі, сумнівно на мобілці, якщо робити це занадто рано.

Рішення: Якщо стиснений файл > ~2–4MB, розгляньте розбиття індексу (за секцією/версією) або перехід на бінарний формат; також переконайтесь, що Brotli віддається коректно.

Завдання 2: Перевірити, чи сервер дійсно віддає Brotli

cr0x@server:~$ curl -I -H 'Accept-Encoding: br' https://docs.example.test/search/index.json
HTTP/2 200
content-type: application/json
content-encoding: br
cache-control: public, max-age=31536000, immutable
etag: "b3f9a2c4"

Значення: Сервер шанує Brotli і використовує immutable-кешування. Добре: браузер може кешувати назавжди й повторно використовувати.

Рішення: Якщо content-encoding відсутній, виправте налаштування CDN/origin для компресії. Якщо кешування коротке — використайте імена файлів з хешем вмісту і встановіть immutable.

Завдання 3: Перевірити статус кешу CDN (HIT vs MISS)

cr0x@server:~$ curl -I https://docs.example.test/search/index.json | grep -i -E 'cache|age|cf-cache-status|x-cache'
cache-control: public, max-age=31536000, immutable
age: 86400
x-cache: HIT

Значення: Індекс кешується на edge і віддавався протягом дня.

Рішення: Якщо часто бачите MISS, вивчіть ключі кешу, query params або часті інвалідації. Edge-miss робить «миттєве» схожим на «колись».

Завдання 4: Підтвердити імена файлів з хешем (immutable)

cr0x@server:~$ ls public/search | head
index.7a9c2f1a.json.br
docs.7a9c2f1a.map.json.br
meta.7a9c2f1a.json

Значення: Імена файлів містять хеш; можна кешувати довго без турбот про оновлення.

Рішення: Якщо ви ще віддаєте index.json з мутованим контентом, перейдіть на імена з хешем і оновіть завантажувач, щоб він отримував актуальний хеш через невеликий meta-файл.

Завдання 5: Перевірити заголовки кешування для meta-файлу (він має бути короткоживучим)

cr0x@server:~$ curl -I https://docs.example.test/search/meta.json | grep -i cache-control
cache-control: public, max-age=300

Значення: Meta-файл може оновлюватись швидко (новий реліз), тоді як хешовані артефакти залишаються незмінними.

Рішення: Якщо meta кешується на рік — клієнти не дізнаються про нові хеші; якщо він некешований — ви додаєте зайву затримку при кожному старті сесії.

Завдання 6: Виміряти час передачі й розмір з типової точки

cr0x@server:~$ curl -o /dev/null -s -w 'size=%{size_download} time=%{time_total} speed=%{speed_download}\n' https://docs.example.test/search/index.7a9c2f1a.json.br
size=3355443 time=0.142 speed=23629670

Значення: ~3.2MB завантажилися за 142ms з цієї точки. Не гарантує того ж для мобільних пристроїв.

Рішення: Якщо час високий, розгляньте менший індекс або передзавантаження тільки на хорошому з’єднанні (Network Information API, але з обережністю).

Завдання 7: Підтвердити, що JSON індексу парситься в межах бюджету (Node як проксі)

cr0x@server:~$ node -e 'const fs=require("fs"); const t=Date.now(); JSON.parse(fs.readFileSync("public/search/index.json","utf8")); console.log("ms="+(Date.now()-t));'
ms=287

Значення: Парсинг займає ~287ms на цій машині. На мобілці може бути гірше.

Рішення: Якщо час парсингу > ~100ms на девмашині, виносьте парсинг з головного потоку (воркер) або змініть формат на швидший для парсингу.

Завдання 8: Перевірити, що бандл воркера дійсно окремий і кешований

cr0x@server:~$ ls -lh public/assets/search-worker.*.js
-rw-r--r-- 1 deploy deploy 54K Jan 12 10:14 public/assets/search-worker.a19c7c0d.js

Значення: Скрипт воркера маленький і може кешуватись. Добре для повторних пошуків.

Рішення: Якщо воркер вбудований у головний JS, розгляньте code-splitting, щоб початкове завантаження не платило за пошук, поки він не потрібен.

Завдання 9: Ідентифікувати довгі таски під час взаємодії з пошуком (експорт трейсів Chrome, аналіз локально)

cr0x@server:~$ node -e 'const fs=require("fs"); const t=JSON.parse(fs.readFileSync("trace.json","utf8")); const long=t.traceEvents.filter(e=>e.name==="Task" && e.dur>50000).length; console.log("long_tasks_over_50ms="+long);'
long_tasks_over_50ms=7

Значення: Є 7 довгих тасків понад 50ms, ймовірна причина лагу введення.

Рішення: Перенесіть логіку запитів і підсвічування у воркер; зменшіть роботу з DOM; переконайтесь, що список результатів віртуалізовано, якщо великий.

Завдання 10: Переконатися, що логи сервера показують, що запити індексу не б’ють по origin

cr0x@server:~$ sudo awk '$7 ~ /search\/index\./ {c++} END{print "index_requests=" c}' /var/log/nginx/access.log
index_requests=43

Значення: Лише 43 запити індексу дісталися цього origin (можливо, CDN виконує свою роботу).

Рішення: Якщо origin бачить потік запитів, ваш CDN-кеш зламався або ви занадто часто міняєте імена індексу.

Завдання 11: Перевірити поведінку ETag (304 має відбуватись для meta; хешовані артефакти — кеш-удари)

cr0x@server:~$ curl -I https://docs.example.test/search/meta.json | awk -F': ' 'tolower($1)=="etag"{print $2}'
"9c3a0f11"

Значення: У meta є ETag. Клієнти можуть ревалідувати дешево.

Рішення: Якщо ETag відсутні — вмикайте їх на origin. Для meta це зменшує байти при збереженні свіжості.

Завдання 12: Перевірити, що індекс випадково не віддається не стисненим в дорозі через помилку конфігурації

cr0x@server:~$ curl -I https://docs.example.test/search/index.7a9c2f1a.json.br | grep -i -E 'content-encoding|content-length'
content-encoding: br
content-length: 3355443

Значення: Стиснений payload віддається як Brotli, і розмір правдоподібний.

Рішення: Якщо бачите гігантський content-length і відсутній encoding, ви платите повну нескомпресовану ціну і, ймовірно, втрачаєте «миттєвість» на холодних завантаженнях.

Завдання 13: Виявити випадкові параметри, що ломають кеш

cr0x@server:~$ sudo grep -R "index.*\?v=" -n public | head
public/assets/app.js:412:fetch("/search/index.json?v="+Date.now())

Значення: Хтось додає Date.now() до URL індексу, гарантуючи пропуски кешу.

Рішення: Приберіть це. Використовуйте імена з хешем або ревалідацію через ETag. Cache-busting — не риса характеру.

Завдання 14: Підтвердити, що Service Worker кешує артефакти пошуку (якщо ви його використовуєте)

cr0x@server:~$ rg -n 'search/index|CacheStorage|workbox' public/sw.js
42:  const SEARCH_ASSETS = ["/search/meta.json", "/search/index.7a9c2f1a.json.br", "/search/docs.7a9c2f1a.map.json.br"];
58:  event.waitUntil(caches.open("docs-search-v1").then(c => c.addAll(SEARCH_ASSETS)));

Значення: Артефакти пошуку явно кешуються, що стабілізує повторну продуктивність.

Рішення: Якщо SW не кешує їх — додайте стратегію кешування; якщо кешує без версіонування — ризикуєте віддавати застарілі індекси після деплоя.

Швидкий плейбук діагностики

Коли пошук перестає відчуватись миттєвим, не думайте довго. Тріаж. Ось найкоротший шлях до вузького місця.

Перше: це завантаження, парсинг, запит чи рендер?

  1. Перевірте, чи індекс закешований (DevTools → Cache Storage/HTTP cache; або дивіться заголовки CDN HIT). Якщо він не в кеші — ваша історія «миттєвості» закінчується.
  2. Виміряйте час готовності індексу: час від фокусу до «індекс завантажений + розпарсений». Якщо це довго — причина в завантаженні/парсингу.
  3. Виміряйте час запиту в ізоляції: виконайте той самий запит 10 разів; якщо він сильно покращується після першого — перший хіт це парсинг/ініціалізація.
  4. Перевірте довгі таски під час набору: якщо введення лагає — ви блокуєте головний потік (рендер/підсвічування/запит на main).

Друге: валідуйте семантику кешування (звичний злодій)

  1. Meta з коротким TTL, хешовані артефакти — immutable
  2. Немає кеш-бастящих query-параметрів
  3. Коректні Content-Encoding і Content-Type
  4. CDN дійсно кешує індекс (не обходиться через куки чи заголовки)

Третє: перевірте зростання індексу і зміни схеми

  1. Розмір індексу підскочив? Ймовірно, зміни у збірці індексують повні тіла або дублюють поля.
  2. Схема змінилась без інкрементації версії? Старий кешований індекс ламає парсинг і викликає fallback-поведінку.
  3. Додана нова мова/версія? Можливо, ви передзавантажуєте занадто багато для всіх.

Четверте: упевніться, що збагачення не саботує UI

  1. Запити збагачення не повинні блокувати локальні результати.
  2. Збагачення не має агресивно перерозставляти список; оновлюйте фрагменти на місці.
  3. Швидко таймаутьте збагачення; не тримайте UI в заручниках заради «приємного, але не обов’язкового».

Поширені помилки (симптом → причина → виправлення)

1) Симптом: Пошук швидкий на Wi‑Fi, жахливий на мобілці

Причина: Ви передзавантажуєте багатомегабайтний індекс при завантаженні сторінки, і мобільні пристрої платять за холодне завантаження + парсинг до того, як користувач почне шукати.

Виправлення: Передзавантажуйте в простий момент з низьким пріоритетом; обмежуйте за якістю з’єднання; розділяйте індекс по секціях; кешуйте через SW. Тримайте meta малим і оновлюваним.

2) Симптом: Введення лагає, символи з’являються із затримкою

Причина: Запит і/або підсвічування виконуються на головному потоці; оновлення DOM важкі; список результатів повністю перерендерюється на кожному натисканні.

Виправлення: Перенесіть пошук у воркер; обмежте результати; віртуалізуйте список; дебаунсьте підсвічування; використовуйте keyed-rendering і уникайте thrash-верстки.

3) Симптом: Користувачі повідомляють «немає результатів» для очевидних термінів

Причина: У збірці індексу відсутні заголовки/титули, або ви неправильно задали область пошуку (не та версія/мова), або індекс застарів у кеші.

Виправлення: Валідуйте pipeline збірки; додайте UI для вибору сфери; версіонуйте схему індексу і інвалідуйте коректно; додайте телеметрію «версія індексу не співпадає».

4) Симптом: Результати перетасовуються під час набору, викликаючи помилкові кліки

Причина: Ранжування нестабільне, і збагачення переставляє результати, коли приходять фрагменти; також UI може не зберігати стан вибору.

Виправлення: Тримайте локальне ранжування стабільним; перерозставляйте лише при явній дії користувача (Enter) або після паузи; мерджте збагачення без повного перетасування.

5) Симптом: Витрати бекенду різко зросли після запуску «миттєвого пошуку»

Причина: Ви все ще звертаєтесь до сервера на кожне натискання для аналітики чи збагачення, або дебаунс занадто малий, або на сервері немає кешування.

Виправлення: Батчируйте аналітику; відправляйте події при виборі, а не під час набору; кешуйте відповіді збагачення; додайте rate limit; використовуйте локально-перший підхід для типового випадку.

6) Симптом: Пошук працює в dev, але інколи ламається в prod

Причина: Мішане кешування meta/index між деплоями: клієнти отримують новий meta але старий індекс (або навпаки), що викликає розбіжності схеми.

Виправлення: Зробіть meta авторитетним для повного набору URL артефактів; забезпечте атомарність деплоя; включіть версію схеми в meta і в телеметрію.

7) Симптом: Проблеми доступності (екрани читачів втрачають фокус, фокус «застряє»)

Причина: Кастомна поведінка listbox/dialog без коректних ARIA-ролей; управління фокусом зроблене «на око».

Виправлення: Використовуйте встановлені ARIA-патерни для combobox/listbox; зберігайте фокус; переконайтесь, що Escape закриває і повертає фокус; тестуйте робочі процеси лише з клавіатурою.

Три корпоративні історії з поля бою

Історія 1: Інцидент через хибне припущення

Команда доків відправила новий яскравий оверлей пошуку. Він відчувався чудово в стенді. «Миттєво», казали вони, і продуктовики кивнули, ніби зрозуміли, що це означає. Реалізація використовувала серверний endpoint, бо «у нас уже є Elasticsearch». UI дебаунсив запити на 100ms і нічого не кешував на клієнті.

Хибне припущення було просте: «Трафік пошуку по доках невеликий». Це було правдою, коли навігація працювала й користувачі були терплячими. Воно перестало бути правдою, коли реліз зламав CLI і всі почали шукати нове ім’я прапорця одночасно.

Під час того тижня бекенд пошуку був завалений запитами від натискань клавіш: короткі, повторювані й некешовані. Латентність росла. UI відповідав спінерами. Користувачі набирали більше. Бекенд отримував ще більше навантаження. Система знайшла нову рівновагу: муки.

SRE був підключений, бо кластер виглядав як інцидент: CPU високий, черги ростуть, таймаути. Але «фікс» не полягав у додаванні нод. Він полягав у переміщенні типової поведінки в локально-перший режим. Вони відправили компактний індекс для заголовків/підзаголовків, запитували локально і зменшили серверні виклики до збагачення при виборі. Навантаження бекенду впало не тому, що кластер став більшим, а тому, що його більшість часу ігнорували.

Історія 2: Оптимізація, що дала відкат

Інша компанія вирішила зробити пошук «розумнішим». Вони ввімкнули агресивну нечіткість і індексували весь текст сторінок локально. Індекс роздувся. На корпоративному Wi‑Fi все ще завантажувалось нормально, тож команда оголосила перемогу.

Потім support став бачив скарги мобільних користувачів, що пошук «зависає». UI не висів; він парсив великий JSON і робив fuzzy-скоринг на головному потоці. На слабших пристроях клавіатура лагала і вся сторінка відчувалась зламаною.

Команда спробувала виправити це, збільшивши дебаунс. Це зменшило кількість запитів, але зробило UI повільним і непередбачуваним: результати приходили хвилями, відривались від набору. Користувачі втратили довіру і почали шукати зовнішніми системами, що призвело до потрапляння на застарілі сторінки і збільшення кількості тикетів. Ідеальний цикл самоштрафування.

Кінцеве вирішення було нудним: стиснути локальний індекс до важливих полів (заголовок/підзаголовки/резюме), винести запити у воркер і перенести fuzzy до рівня 2 (мережа) за коротким таймаутом. Вони зберегли «розумну» поведінку, але тільки там, де вона не саботує чутливість введення.

Історія 3: Нудна, але правильна практика, що врятувала ситуацію

Сайт документації платформи мав кілька версій і мов. Команда була дисциплінована: кожен артефакт пошуку мав версію схеми, кожен деплой був атомарним, а meta-файл був єдиною змінною точкою. Вони також логували клієнтську телеметрію: версію індексу, час завантаження, статус кешу і чи використовувався воркер.

Одної п’ятниці реліз ввів тонку зміну схеми індексу: поле було перейменоване в мапі документів. У багатьох командах це було б інцидентом — клієнти з застарілим кешем отримують невідповідні артефакти, воркер падає і віддає «немає результатів».

Цього разу телеметрія зафіксувала це за кілька хвилин: сплеск подій «несумісність схеми індексу», згрупованих по старішому кешу Service Worker. Оскільки артефакти версіонувались, команда безпечно зробила форвард: підняли версію схеми, оновили ім’я кеша SW і задеплоїли. Клієнти оновили meta, побачили нові URL артефактів і отримали чистий кеш.

Без екстреного rollbackа. Без нічних дзвінків. Просто спокійне виправлення і невелика постмортем-нота: «Версіонуйте артефакти. Це не гламурно. Це те, як ви спите нормально.»

Чек-листи / покроковий план

Покроково: впровадьте патерн без драм

  1. Визначте швидкий шлях: заголовки + підзаголовки + резюме; обмежте результати до 10–20.
  2. Згенеруйте компактний індекс: створюйте під час збірки; виробляйте артефакти з хешем; генеруйте невеликий meta-файл, що вказує на поточний хеш.
  3. Віддавайте індекс коректно: Brotli ввімкнений, правильний content-type, immutable-кешування для хешованих файлів, короткий TTL для meta.
  4. Передзавантажуйте обдумано: idle-time prefetch; пріоритет підвищувати, коли користувач фокусується на полі пошуку.
  5. Винесіть запит у воркер: тримайте головний потік для введення + рендера; застосуйте часові бюджети.
  6. Рендерте прогресивно: показуйте заголовки/хлібні крихти спочатку; фрагменти підвантажуйте асинхронно.
  7. Стабілізуйте ранжування: детермінований сортування; уникайте джиттера; обробляйте збагачення без перетасовування.
  8. Інструментуйте метрики: TTFR, час готовності індексу, коефіцієнт попадання в кеш, тривалість запиту, довгі таски, нульові результати.
  9. Додайте запобіжники: таймаути, фолбеки і стан «прогрів пошуку», що не блокує введення.
  10. Тестуйте на повільних пристроях: симулюйте уповільнення CPU; перевіряйте, що введення залишається чутливим.

Чек-лист для релізу: уникайте самозавданих інцидентів

  • Версія схеми індексу підвищується при зміні полів
  • Meta-файл має cache-control у хвилинах, а не в днях
  • Хешовані артефакти мають immutable з довгим max-age
  • Немає query-param cache-busting
  • Шлях до воркера протестований у production build
  • Дашборди телеметрії оновлені для нових полів
  • Перевірка регресії розміру індексу (завалювати збірку при незапланованому стрибку)

UX-чек-лист: зробіть так, щоб це відчувалось миттєво, не брешучи

  • Введення ніколи не лагає
  • Результати з’являються в передбачуваний бюджет на кешованому шляху
  • Стабільна верстка списку; без великих переробок
  • Клавіатурна навігація працює
  • Чіткий індикатор сфери (версія/мова/продукт)
  • Стан «нуль результатів» чесний і дійовий

FAQ

1) Використовувати клієнтський пошук чи хостинґований сервіс?

Використовуйте клієнтський підхід для швидкого шляху (заголовки/підзаголовки). Додавайте хостинґований пошук для збагачення, толерантності до опечаток і пошуку по всіх ресурсах. Гібрид перемагає дотримання чистоти.

2) Наскільки великим може бути індекс, поки патерн перестає працювати?

Універсального числа немає, але коли стиснений індекс + парсинг перевищують ваш бюджет TTFR — користувачі це відчують. Розділяйте за секціями/версіями або переходьте на бінарний/стрімінговий формат.

3) Чи завжди JSON — погана ідея для індексу?

Не завжди. Малий JSON з Brotli може бути нормальним. Проблема виникає, коли парсинг блокує головний потік або структура глибоко вкладена і величезна.

4) Чому не просто запитувати сервер на кожне натискання з дебаунсом?

Бо дебаунс не вирішує хвостової затримки, офлайн-поведінки або посилення навантаження бекенду під час релізів. Локально-перший підхід робить продуктивність передбачуваною.

5) Чи справді потрібен Web Worker?

Якщо ви хочете «миттєвого» на телефонах і ваш індекс більше за крихітний — так. Воркери — недороге страхування від зависань головного потоку.

6) Як уникнути стрибків результатів, коли приходить збагачення?

Рендерте локальні результати зі стабільним порядком. Коли збагачення повертає дані, оновлюйте метадані/фрагменти на місці. Якщо потрібно перерозставити, робіть це лише після паузи або при явній відправці.

7) Які метрики поставити на дашборд?

Час готовності індексу (p50/p95), TTFR (p50/p95), тривалість запиту, коефіцієнт попадань у кеш, частка нульових результатів і кількість довгих тасків під час взаємодії з пошуком.

8) Як обробляти кілька версій документів без завантаження всього?

Зробіть сферу явною і завантажуйте індекси по сферам. Використовуйте малий реєстр meta і підвантажуйте потрібний шард для вибраної версії/мови.

9) А як щодо документів із обмеженим доступом?

Не віддавайте приватний контент у публічному індексі. Для автентифікованого середовища тримайте локальний індекс лише з метаданими безпечними для публіки і покладайтеся на серверну перевірку для обмежених результатів.

10) Чи можна зробити це офлайн-дружнім?

Так. Кешуйте індекс і воркер через Service Worker. Офлайн-пошук добре працює для заголовків/підзаголовків; збагачення фрагментів можна пропустити або віддавати з кешованих сторінок.

Висновок: практичні наступні кроки

Якщо ваш пошук по доках не відчувається миттєвим, не починайте з заміни бібліотек. Почніть з виправлення форми системи: локально-перші результати з кешованим, компактним індексом, виконуваним поза головним потоком і прогресивно відрендереним зі стабільним UI.

Наступні кроки, які ви можете виконати цього тижня:

  1. Виміряйте TTFR і час готовності індексу на середньому телефоні.
  2. Зробіть артефакти пошуку з хешем вмісту і immutable; зробіть meta короткоживучим.
  3. Перенесіть виконання запитів у воркер і обмежте роботу на натискання клавіші.
  4. Розділіть індекс, якщо стиснений розмір зростає.
  5. Додайте швидкий діагностичний дашборд: коефіцієнт попадань у кеш, TTFR p95, довгі таски і причини нульових результатів.

Мета не в демонстрації. Мета — це поле для пошуку, що поводиться як інфраструктура: тихо, швидко й надійно — особливо коли ваші користувачі вже роздратовані.

← Попередня
Ethernet застряг на 100 Мбіт/с: міф про кабель і реальне вирішення
Наступна →
Застрягли на «Preparing Automatic Repair»? Відновлення, яке справді працює

Залишити коментар