Cmd+K: модальне вікно пошуку — списки результатів, підказки клавіатури та порожні стани (HTML/CSS‑перший)

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

Якщо ваша палітра пошуку Cmd+K виглядає «нормально» на швидкому ноутбуці, але «таємниче зіпсована» всюди інде — це не ілюзія. Модальні вікна пошуку — дивне перетинання полірованого інтерфейсу, вимог доступності та продукційної затримки: ви можете відправити гарний список, який руйнується, щойно обсяг даних зростає або фокус клавіатури йде не туди.

Це інструкція для побудови Cmd+K палітри, яка поводиться як хороший інженер на чергуванні: передбачувано під навантаженням, чесно коли не може допомогти і ніколи не затримує користувача в темній кімнаті без клавіші Escape.

Аудиторія frontend-інженери, дизайнери, які випускають продукт, та всі, хто коли-небудь казав «це ж просто модальне вікно» і жалкував про це.

Охоплення HTML/CSS‑перші патерни інтерфейсу для списків результатів, підказок клавіатури та порожніх станів, а також операційна діагностика і режими відмови.

Як виглядає «добре» в Cmd+K

Палітра пошуку Cmd+K — не іграшкова функція. Вона стає прихованими парадними дверима до вашого продукту. Коли вона хороша, користувачі виробляють м’язову пам’ять і перестають шукати в навігації. Коли вона погана — вони втрачають довіру й перестають користуватися нею. А коли люди перестають шукати, ваша ретельно продумана інформаційна архітектура стає єдиним виходом — що, м’яко кажучи, амбіційно.

Непорушні вимоги до поведінки

  • Миттєвий відгук на кожен натиск клавіші. Якщо потрібне завантаження — покажіть його. Не зависайте.
  • Клавіатурна пріоритетність: / переміщують вибір; Enter активує; Esc закриває; Tab не телепортує фокус в нікуди.
  • Передбачувана ранжировка: один і той самий запит дає ті самі топ‑результати, якщо дані не змінилися.
  • Доступна семантика: скрінрідери повинні отримувати зрозумілу історію: «Пошук, 12 результатів, вибрано результат X.»
  • Чіткий порожній стан, який каже користувачу, що робити далі, а не в чому він неправий.

Як це ламається в продакшені

  • Втеча фокусу: модальне відкривається, але фокус залишається позаду. Користувачі клавіатури вводять текст у попередньо сфокусованому елементі.
  • Караул зі скролом: сторінка позаду модального все ще скролиться; список результатів — ні; хтось відкриває тикет “пошук не працює”.
  • Брехня про затримку: UI показує «Немає результатів» до повернення мережі; потім результати з’являються. Користувачі вчаться ігнорувати це.
  • Шторм подій: кожен натиск клавіші викликає бекенд-пошук; ваш API стає ненавмисним кейлоггером з рахунком.
  • Несумістність підказки і реальності: футер каже, що Enter відкриває, але Enter відправляє форму і закриває модальне.

«Надія — це не стратегія.»

— генерал Гордон Р. Салліван

Суха правда: палітра пошуку — як пейджер. Вона тиха, коли все добре, і коли потрібна — потрібна зараз. Будуйте її так, ніби ви будете дебажити о 2-й ранку з одним відкритим оком.

Факти та історичний контекст для рішень

Ось конкретні історичні фрагменти й індустрійні тренди, які пояснюють, чого користувачі очікують від Cmd+K. Це не дрібниці. Це обмеження, що перевдягаються у цікаві факти.

  1. Командні палітри не народилися у вебі. Просунуті редактори (зокрема IDE) нормалізували «набирай для пошуку команд» задовго до браузерів, тож користувачі приходять зі сформованими очікуваннями щодо поведінки клавіатури.
  2. Spotlight популяризував «пошук як запускатор». Системний пошук навчив людей, що пошук — це не лише знаходження документів, а універсальний вибір дії.
  3. Cmd+K став де-факто конвенцією в сучасному вебі, бо легко запам’ятовується, не конфліктує з «Знайти на сторінці» (Cmd+F) і платформи потребували спільної м’язової пам’яті.
  4. Патерни WAI-ARIA для combobox/listbox еволюціонували повільно, бо доступний «typeahead + list» виявився дивно складним; багато ранніх патернів ламали скрінрідери або навігацію клавіатурою.
  5. Модальні діалоги мають довгу історію багів фокусу, бо фокус-трепінг — не нативна річ у браузері; ви імітуєте менеджер вікон на сторінці.
  6. Очікування «миттєвого пошуку» відстежують прогрес апаратного забезпечення. З ростом швидкості пристроїв толерантність до «почекати після кожного натискання» зникла; UX‑норми ускладнилися.
  7. CDN зробили доставку активів дешевою, але не доставку стану. Вивантажити великий індекс на клієнт може виглядати «швидко» локально, а потім спалити слабкі пристрої пам’яттю.
  8. Мобільні змінили значення підказок клавіатури. Футер, повний кейкапів, — шум на сенсорних пристроях; підказки мають адаптуватися.

Рішення: розглядайте Cmd+K як функцію м’язової пам’яті. Ваш основний KPI — не «час на впровадження», а «час до успіху на запит при навантаженні».

Анатомія: поле введення, список результатів, підказки внизу та стани

Cmd+K модальне — маленький інтерфейс, але в нього є окремі підсистеми. Якщо ви їх не назвете, будете відлагоджувати їх як один клубок. Назвіть — це допомагає.

Підсистема Завдання Режим відмови Філософія виправлення
Тригер Надійно відкривати звідусіль Конфлікти шорткатів, блокування в полях введення Повага до платформних конвенцій; не захоплюйте поля вводу
Оболонка діалогу Уловлювати фокус; запобігати взаємодії з бекграундом Витік фокусу, прокрутка фону Використовуйте правильну семантику; блокувати скрол
Поле пошуку Захоплювати запит; показувати завантаження; очищувати Помилки IME, неправильний дебаунс Обробляти композицію; не надто агресивно дебаунсити
Список результатів Показувати; дозволяти вибір; активувати Ривкова прокрутка, неправильний вибір Простий DOM, стабільні ключі, передбачувана підсвітка
Футер з підказками Навчати взаємодії; показувати область пошуку Підказки брешуть або перевантажують Показуйте лише те, що правда; адаптуйте по пристрою
Стани Порожній, помилка, завантаження, офлайн Невизначене «нічого не сталося» Завжди комунікуйте наступну дію

Більшість палітр — це 70% поведінки списку результатів, 20% семантики фокусу, 10% решти. Саме список — місце, куди йдуть на цвинтар UX‑мрії, бо там зустрічаються затримка, ранжування, доступність і людське нетерпіння.

Структура HTML/CSS‑перший (progressive enhancement)

HTML/CSS‑перший не означає «без JavaScript». Це означає, що розмітка виражає намір, стани видимі, і JS підсилює поведінку, а не вигадав її з нуля. В термінах надійності: ви хочете деградацію з гідністю і спостережувані стани.

Базова розмітка: dialog + input + list + footer

Якщо можете — використайте реальний dialog, але сприймайте його як UI‑примітив, а не як магічний експеримент. Вам все одно потрібне управління фокусом і блокування скролу навколо нього. Ваша HTML має мати сенс, навіть якщо логіка вибору падає.

Демо: список результатів з підказками клавіатури

Ранбук: затримка API
Операції · Оновлено 2 дні тому
Enter
Дашборд: коефіцієнт кеш‑попадань
Нагляд · В реальному часі
Enter
Сервіс: billing-worker
Сервіси · Раніше деградований
Enter

Це «HTML‑перший» у дусі: читабельна структура списку, видимий вибір і підказки, що відповідають поведінці.

Демо: порожній стан, що підказує наступний крок

Немає збігів для «kafak».

  • Спробуйте kafka або queue.
  • Використовуйте префікси /, наприклад /runbook, щоб звузити область.
  • Якщо ви очікували сервіс, він може бути прихований через права доступу.

Порожні стани мають зменшувати невизначеність: «це мій запит, система чи права доступу?»

Порожній стан — це продуктова поверхня. Ставтеся до неї відповідально.

Семантика доступності, яку не варто імпровізувати

Виберіть відомий ARIA‑патерн і слідуйте йому. Для «typeahead + список результатів» зазвичай обирають патерн, схожий на combobox, або простіший textbox + listbox з active descendant. Конкретний патерн залежить від того, чи є результати «підказками», чи це окремий список результатів. Що б ви не обрали, переконайтеся, що скрінрідери можуть оголосити:

  • Де зараз фокус (поле вводу або список)
  • Скільки результатів існує
  • Який елемент вибрано

Погляд: якщо у вашого додатку вже є зріла a11y‑інфраструктура, реалізуйте повний active‑descendant патерн. Якщо ні — залишайтеся простими, але коректними: не випускайте напів‑combobox.

Жарт наостанок, коротко і практично: модальне без управління фокусом — як дата‑центр без дверей: технічно «відкритий», операційно — жахливий.

Список результатів, що переживе реальність

Списки результатів у командних палітрах часто будують як стрічку соцмереж: купа вкладених елементів, іконки, метадані, теги, підсвітка, кнопки, меню на ховері. Потім хтось питає, чому навігація стрілками підвисає. Бо ви побудували міні‑DOM‑катедру і просите його перераховуватися 60 разів на секунду.

Правила для списку, що лишається швидким

  • Тримайте DOM рядка поверхневим. Рядок має містити: заголовок, опціональний фрагмент і підказку праворуч. Не складний вкладений UI‑фреймворк.
  • Стабільна ідентичність. Не використовуйте індекси як ключі. Використовуйте стабільний ідентифікатор або шлях URL. Інакше вибір буде стрибати при оновленні результатів.
  • Вибір — це стан, а не hover‑ефект. Використовуйте aria-selected і видимий стиль, що працює без наведення.
  • Прокручуйте список, а не сторінку. Дайте контейнеру результатів max-height і overflow:auto.
  • Не підсвічуйте, перебудовуючи innerHTML. Використовуйте функцію рендеру, що безпечно розбиває текст, або попередньо обчислюйте діапазони підсвітки. innerHTML — там, де XSS і проблеми продуктивності знайомляться.

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

Є дві поширені моделі. Виберіть явно:

  • Фокус лишається в полі вводу, а стрілки змінюють «active descendant» у списку. Це зберігає стабільність набору і полегшує роботу з IME/композицією. Це також складніше в плані a11y.
  • Фокус переходить у список при першому натисненні стрілки вниз і повертається в інпут при наборі. Простіша семантика, але треба забезпечити, щоб набір не губився, і відновлення фокусу було коректним.

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

Ранжування й групування без введення користувача в оману

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

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

Підказки клавіатури: показуйте їх без крику

Підказки клавіатури — це документація UI, яку ви відправляєте в продакшен. Отже вони мають бути:

  • Правдиві (відповідають реальній поведінці)
  • Контекстні (не показуйте «Enter щоб відкрити», якщо нічого не вибрано)
  • Адаптивні (не нав’язуйте кейкапи на сенсорних пристроях; не показуйте Cmd на Windows)

Рендер кейкапів, що не виглядає як записка з головоломки

Використовуйте простий CSS для кейкапів. Уникайте inline SVG для кожної клавіші. Ви роздуєте DOM і ускладните темізацію. Тримайте компоненти кейкапів послідовними: рамка, фон і легка внутрішня тінь. Це дрібниця, але робить палітру відчутно «нативною».

Підказки як машина станів

Підказки мають відображати стан:

  • Без дій (ще немає запиту): показуйте приклади (/ для області або «Наберіть для пошуку…»)
  • Пошук: показуйте «Пошук…» і Esc щоб закрити
  • Є результати: показуйте навігацію + клавіші дії
  • Порожньо: покажіть, як розширити запит, і за потреби fallback «пошук по всьому»
  • Помилка/офлайн: покажіть клавішу повторення або fallback «Відкрити в пошуку браузера»

І ще один жарт: якщо ваш футер каже «Натисніть Esc щоб закрити», а Esc не закриває — вітаю, ви винайшли тест на регрес довіри.

Порожні стани: мовчання — це баг

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

Три порожні стани, що потрібні (не лише один)

  • Немає збігів: запит повернув нуль результатів. Надайте підказки та поясніть область пошуку.
  • Ще не проіндексовано: дані існують, але ще не доступні для пошуку. Скажіть про це і дайте альтернативу.
  • Обмежено доступом: користувач може не бачити елементи через права. Визнайте це без витоку конфіденційної інформації.

Контент порожнього стану, що зменшує кількість тикетів

Хороші порожні стани відповідають на три питання:

  1. Чи система мене почула? Повторіть запит (очищений, якщо потрібно).
  2. Де шукали? «Тільки документи» проти «Усе».
  3. Що далі? Поради, оператори області або спосіб запитати доступ.

Операційний кут: якщо ви не можете відрізнити «нема результатів» від «сервер пошуку таймаутнув», ви тижнями будете ганяти тикети «пошук нестабільний», що насправді UX‑неясність.

Коли «Немає результатів» — брехня

Дві класичні причини:

  • Умови гонки: ви показали порожній стан для швидкої відповіді старого запиту, а потім перезаписали його результатами нового запиту або навпаки.
  • Надто агресивний дебаунс: UI затримує запити, але локальна фільтрація одразу показує порожній стан, тож він коротко з’являється при кожному натисканні клавіші.

Виправте це послідовністю запитів (монотонні ID запитів) і зв’язуванням станів з тим самим життєвим циклом: якщо ви дебаунсите запити, дебаунсьте і переходи в порожній стан.

Продуктивність і надійність: нудні обмеження

Cmd+K виглядає як фронтенд‑фіча, поки не виводить з ладу ендпоінт пошуку, і раптом це вже SRE‑фіча. Потрібні бюджети та зворотний тиск.

Бюджети затримки: до чого прагнути

  • Відкриття модального: менше 100 мс, включно з розміщенням фокусу.
  • Перші результати після набору: сприймано менше 150–250 мс, бажано з оптимістичними локальними результатами, якщо доступні.
  • Навігація стрілками: має бути миттєвою; будь‑який ривок — баг.

Зворотний тиск: не DDoSіть себе самі

Набір генерує сплески трафіку. Якщо ви викликаєте бекенд на кожному натисканні, використайте:

  • Дебаунс (малий, наприклад 80–150 мс) і/або тротл
  • Скасування (abort для незавершених запитів)
  • Кешування на клієнті для останніх запитів
  • Серверні ліміти, що повертають дружній відповідь, а не 429‑істеричку

Обсервабіліті: меряйте важливе

Вимірюйте:

  • Час до відкриття (keydown → input сфокусовано)
  • Час до перших результатів (зміна запиту → список заповнено)
  • Частоту порожніх станів, по області і по сімейству user agent
  • Рівень помилок і таймаутів (і чи UI показував «Немає результатів» замість цього)
  • Кількість запитів на користувача (спайки означають, що дебаунс або кеш зламався)

Трюк SRE: думайте про палітру як про клієнт, що генерує навантаження. Це не просто UI. Це генератор трафіку з під’єднаною клавіатурою.

Три корпоративні історії з війни

1) Інцидент через хибне припущення: «Результати пошуку все одно публічні»

Середня за розміром компанія створила єдину Cmd+K палітру, що шукала документи, тикети і внутрішні дашборди сервісів. Команда припустила, що якщо елемент є в навігації — його можна показувати в пошуку. Це спрацювало для документів. Провалилось для тикетів і сервісних метаданих.

Проблема не була витоком контенту. Це був витік метаданих. Список результатів показував заголовки й назви проєктів для елементів, які користувач не міг відкрити. «Доступ заборонено» з’являвся після вибору, що, на думку команди, було прийнятним. Насправді сам заголовок містив чутливі підказки: назви інцидентів, імена клієнтів, кодові назви поглинань. Палітра стала випадковим каналом пліток.

Баг пройшов код‑рев’ю, бо всі тестували з адмінськими акаунтами. Він пройшов стаджинг, бо дані в ньому були санітайзені. І пройшов реліз, бо «ніхто не скаржився», поки одна людина не заявила про це голосно, на не дуже бажаній нараді.

Виправлення було не лише в додаванні перевірок прав. Змінили UI‑контракт: результати повертали лише ті елементи, які користувач може відкрити, а для прикордонних випадків повертали загальний запис «Обмежений елемент» без ідентифікуючих метаданих. Додали також підказку у порожньому стані: «Якщо ви очікували результат, можливо, у вас немає доступу.» Одне речення знизило кількість тикетів «пошук не працює» і зробило позицію безпеки очевидною.

2) Оптимізація, що відкотилася: попереднє індексування на клієнті, щоб «зробити миттєво»

Інша команда вирішила доставити передобчислений індекс у браузер, щоб Cmd+K палітра працювала офлайн і відчувалася миттєво. В демо це спрацювало. Індекс був стиснений, доставлявся через CDN і агресивно кешувався. UI був як блискавка на MacBook.

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

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

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

3) Нудна, але правильна практика, що врятувала ситуацію: feature flag + canary + error budget

Велике підприємство розгортало нову реалізацію командної палітри в кількох внутрішніх застосунках. UI‑команда зробила правильні нудні речі: feature flag, канарні когорти і чітке SLO: «Рівень помилок запитів на пошук < X, p95 затримка < Y». Ніяких героїчних вчинків. Просто дисципліна.

Під час канарки метрики показали дивний сплеск: помилки збільшилися лише для певного регіону і лише в понеділок вранці. UI виглядав нормально. Команда могла списати це на бекенд і рухатись далі. Натомість вони зв’язали сплеск зі специфічною поведінкою клавіатури: люди відкривали палітру і вставляли довгі рядки (пачки тикетів, нотатки зустрічей), що вибухало складністю запитів вниз по стеку.

Виправлення не було гламурним: обмеження довжини введення з дружнім повідомленням і серверні захисні обмеження запитів. Канарка запобігла широкому падінню, а feature flag дозволив швидко відключити вражені додатки, поки патч не ввійшов у дію.

Це не був «крутий UI‑він». Це була невелика операційна перемога, що запобігла інциденту, який створив би більше роботи з обсервабіліті, ніж хотілося б за квартал.

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

UI‑баґи часто живуть на межі між поведінкою фронтенду і реальністю бекенду. Ці завдання — те, що ви виконуєте, коли хтось каже «Cmd+K повільний» або «пошук нічого не показує» і ви хочете припинити гадати. Кожне завдання містить команду, що означає вивід, і рішення.

1) Переконатися, що бандл UI не виріс

cr0x@server:~$ ls -lh /srv/web/assets/search-palette.*.js
-rw-r--r-- 1 root root 412K Dec 18 09:12 /srv/web/assets/search-palette.8b2c1a.js

Що це означає: чанк палітри — 412K на диску (до стиснення). Якщо раніше було 120K, ви, ймовірно, запхали залежність‑бомбу.

Рішення: Якщо чанк суттєво виріс, аудит імпортів (бібліотеки fuzzy search, пакети іконок) і рознесення «рідкісних» фіч (підсвітка, недавні елементи) за асинхронними межами.

2) Перевірити gzip/brotli ефективність для чанку

cr0x@server:~$ gzip -c /srv/web/assets/search-palette.8b2c1a.js | wc -c
118902

Що це означає: Gzipped ~119KB. Це реалістично. Якщо gzip майже як сирий розмір — файл, можливо, вже погано мінімізований або містить багато нестисливої інформації.

Рішення: Якщо стиснення неефективне — приберіть вбудовані набори даних, великі JSON‑блоки або base64‑ресурси з JS‑бандла.

3) Перевірити, що сервер віддає brotli, коли потрібно

cr0x@server:~$ curl -I -H 'Accept-Encoding: br' https://app.example.internal/assets/search-palette.8b2c1a.js
HTTP/2 200
content-type: application/javascript
content-encoding: br
cache-control: public, max-age=31536000, immutable

Що це означає: Brotli активний. Якщо ви не бачите content-encoding: br, ваш «швидкий» UI може переплачувати за доставку.

Рішення: Налаштуйте CDN/origin перед тим, як чіпати код UI. Пропускна здатність — найглупіший вузький місце і найпоширеніша проблема.

4) Відокремити таймаути бекенду від неоднозначності «Немає результатів»

cr0x@server:~$ tail -n 20 /var/log/nginx/access.log | grep "/api/search" | tail -n 5
10.2.4.19 - - [28/Dec/2025:10:31:44 +0000] "GET /api/search?q=kafka HTTP/2.0" 200 4821 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:45 +0000] "GET /api/search?q=kafak HTTP/2.0" 504 164 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:45 +0000] "GET /api/search?q=kaf HTTP/2.0" 200 9912 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:46 +0000] "GET /api/search?q=kafak%20runbook HTTP/2.0" 504 164 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:47 +0000] "GET /api/search?q=kafka%20runbook HTTP/2.0" 200 10532 "-" "Mozilla/5.0"

Що це означає: Ви бачите 504 для певних запитів. Якщо UI перетворює це в «Немає результатів», користувачі подумають, що індексування зламалось.

Рішення: Оновіть UI, щоб він показував стан помилки на не‑200 відповідях, і додайте серверні захисні обмеження складності запитів.

5) Швидко виміряти розподіл латентності API

cr0x@server:~$ awk '$7 ~ /\/api\/search/ {print $(NF-1)}' /var/log/nginx/access.log | tail -n 10
0.021
0.034
0.112
0.487
1.902
0.029
0.041
0.055
0.078
0.090

Що це означає: Ці числа (якщо формат логів містить час запиту передостаннім полем) показують випадкові відповіді в секунди. Палітра буде відчуватися «нестабільною», навіть якщо середнє в нормі.

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

6) Переконатися, що rate limiting не карає за бурсти набору

cr0x@server:~$ grep -E " 429 " /var/log/nginx/access.log | grep "/api/search" | tail -n 5
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runb HTTP/2.0" 429 89 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runbo HTTP/2.0" 429 89 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runboo HTTP/2.0" 429 89 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runbook HTTP/2.0" 200 10532 "-" "Mozilla/5.0"

Що це означає: Сплеск запитів отримує тротлінг, поки останній запит не пройде. UI буде «мигати» порожнім/помилковим станом.

Рішення: Дебаунс на клієнті, але також налаштовуйте ліміти, щоб дозволяти короткі сплески на користувача. Лімітування має захищати інфраструктуру, а не карати набір тексту.

7) Перевірити, чи запити пошуку кешуються

cr0x@server:~$ curl -I "https://app.example.internal/api/search?q=runbook"
HTTP/2 200
content-type: application/json
cache-control: private, max-age=0
vary: authorization

Що це означає: Не кешується. Іноді це правильно (персоналізовані результати), іноді — марнотратно (публічні документи).

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

8) Переконатися, що бекенд пошуку здоровий (systemd)

cr0x@server:~$ systemctl status search-api.service --no-pager
● search-api.service - Search API
     Loaded: loaded (/etc/systemd/system/search-api.service; enabled)
     Active: active (running) since Sun 2025-12-28 09:41:10 UTC; 52min ago
   Main PID: 2147 (search-api)
      Tasks: 24
     Memory: 612.4M
        CPU: 18min 22.118s

Що це означає: Сервіс працює; пам’ять значна. Якщо пам’ять росте без зупину — можлива витік кешу або невпорядковані множини результатів.

Рішення: Якщо пам’ять підозріла — інспектуйте heap/метрики; обмежте кеші; накладайте максимальні розміри відповіді.

9) Виявити насичення CPU, що корелює зі штормами набору

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.1.0 (search01)  12/28/2025  _x86_64_  (8 CPU)

10:34:21 AM  CPU   %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
10:34:22 AM  all   78.12 0.00  9.34   0.17 0.00  0.42   0.00   0.00   0.00 11.95
10:34:23 AM  all   82.55 0.00 10.02   0.12 0.00  0.38   0.00   0.00   0.00  6.93
10:34:24 AM  all   80.10 0.00  9.77   0.09 0.00  0.40   0.00   0.00   0.00  9.64

Що це означає: CPU активно використовується в userspace. Це узгоджується з дорогим ранжуванням, fuzzy matching або завантаженням індексу на запит.

Рішення: Профілюйте обробку запитів бекенду; попередньо завантажуйте індекси; зменште алокації на запит; обмежте fuzzy‑алгоритми для коротких запитів.

10) Визначити, чи I/O‑bound ви (затримки зберігання)

cr0x@server:~$ iostat -xz 1 3
Linux 6.1.0 (search01)  12/28/2025  _x86_64_  (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          71.20    0.00    8.90    9.80    0.00   10.10

Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %util  await
nvme0n1         980.0   220.0  7840.0  1960.0     0.0     0.0   92.0   7.80

Що це означає: Високий %util і помітний await. Якщо бекенд пошуку читає диск для кожного запиту (індекс на диску, холодний кеш), отримуватимете p95‑спайки, які відчують користувачі.

Рішення: Тримайте гарячі індекси в пам’яті; налаштуйте page cache; зменшіть випадкові читання; або перейдіть на движок пошуку, призначений для цього навантаження.

11) Перевірити мережеві проблеми між UI і API

cr0x@server:~$ ss -s
Total: 1382
TCP:   921 (estab 612, closed 245, orphaned 3, synrecv 0, timewait 245/0), ports 0

Transport Total     IP        IPv6
RAW       0         0         0
UDP       42        36        6
TCP       676       498       178
INET      718       534       184
FRAG      0         0         0

Що це означає: Високий timewait може вказувати на багато короткоживучих з’єднань. Якщо HTTP keepalive погано налаштований, кожне натискання може відкривати нове з’єднання.

Рішення: Виправте keepalive на балансувальнику або Nginx; намагайтеся використовувати HTTP/2; зменшіть накладні витрати на запит.

12) Перевірити, чи UI не викликає пошук, коли модальне закрите

cr0x@server:~$ journalctl -u search-api.service --since "10 minutes ago" | grep "q=" | tail -n 8
Dec 28 10:28:01 search01 search-api[2147]: request_id=2b7b q=runbook user=anon status=200
Dec 28 10:28:02 search01 search-api[2147]: request_id=2b7c q=runboo user=anon status=200
Dec 28 10:28:02 search01 search-api[2147]: request_id=2b7d q=runbook user=anon status=200
Dec 28 10:28:03 search01 search-api[2147]: request_id=2b7e q= user=anon status=400
Dec 28 10:28:03 search01 search-api[2147]: request_id=2b7f q= user=anon status=400
Dec 28 10:28:04 search01 search-api[2147]: request_id=2b80 q= user=anon status=400
Dec 28 10:28:05 search01 search-api[2147]: request_id=2b81 q= user=anon status=400
Dec 28 10:28:06 search01 search-api[2147]: request_id=2b82 q= user=anon status=400

Що це означає: Повторно відправляються пусті запити (q=). Часто це баг життєвого циклу UI: очищення інпуту тригерить запит навіть при закритому модалі.

Рішення: Додайте клієнтський захист: не запитуйте порожній рядок; не запитуйте, коли модальне закрите; скасовуйте незавершену роботу при закритті.

13) Інспектувати повільні SQL‑запити, якщо пошук залазить у БД

cr0x@server:~$ sudo -u postgres psql -c "select now() - query_start as age, state, left(query,120) from pg_stat_activity where datname='searchdb' order by query_start asc limit 5;"
   age   | state  | left
---------+--------+--------------------------------------------------------------
 00:00:05| active | select id,title from docs where title ilike '%kafka runbook%' l
 00:00:02| active | select id,title from docs where title ilike '%kafka runb%' limi
 00:00:01| active | select id,title from docs where title ilike '%kafka run%' limit

Що це означає: Ви виконуєте wildcard ILIKE на кожному натисканні. Це передбачуваний шлях до витрат у БД і суму смутку.

Рішення: Перейдіть на повнотекстовий індекс, або хоча б обмежте запити (prefix search, trigram index) і сильніше дебаунсіть.

14) Перевірити співвідношення хітів/місів кешу, якщо використовуєте Redis

cr0x@server:~$ redis-cli info stats | egrep "keyspace_hits|keyspace_misses"
keyspace_hits:1829012
keyspace_misses:712334

Що це означає: Місів багато відносно хітів. Якщо кеш не допомагає — ви виконуєте зайву роботу даремно.

Рішення: Перегляньте ключі кешу (нормалізуйте запити), TTL і чи кешувати per‑user чи глобально. Якщо персоналізація вбиває кеш — кешуйте лише публічну підмножину.

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

Коли в чаті з’являється «Cmd+K повільний», у вас дві задачі: зупинити кровотечу й знайти реальний вузький профіль. Не починайте з переписування ранжування. Почніть з доказів, куди йде час.

Перше: вирішити, UI‑джанк чи затримка бекенду

  • Симптоми UI‑джанку: навігація стрілками підвисає, набір гальмує, підсвітка вибору «перескакує», CPU на клієнті ривко зростає.
  • Симптоми затримки бекенду: набір гладкий, але результати з’являються пізно; «завантаження» тримається; повтори допомагають; помилки корелюють зі сплесками трафіку.

Швидка перевірка: шукайте 5xx/429 в логах доступу для /api/search і порівнюйте з повідомленнями користувачів.

Друге: перевірити патерни запитів (можливо, ви спамите)

  • Відправляєте запити для порожніх рядків?
  • Відправляєте запити, коли модальне закрите?
  • Скасовуєте незавершені запити?
  • Є кешування для повторюваних часткових запитів (k, ka, kaf)?

Третє: ізолювати повільний шар

  • Якщо бекенд повільний: перевірте CPU (mpstat), I/O (iostat), активність БД (pg_stat_activity), хіт‑рейт кеша (Redis), логи на предмет сплесків складності запитів.
  • Якщо UI повільний: зменшіть складність рендеру списку, уникайте перебудови всього списку при зміні вибору, і переконайтеся, що логіка підсвітки не має складності O(n*m) на кожен натиск клавіші.

Он‑колл евристика: якщо бачите 429s — спочатку виправляйте частоту запитів клієнта. Якщо бачите 504s — фіксуйте таймаути і захисні обмеження. Якщо ні того, ні іншого — ймовірно, UI‑джанк.

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

1) «Набір здається затриманим»

Симптом: символи з’являються з затримкою; курсор ривко рухається.

Причина: синхронна робота на кожному натисканні (рендер занадто багатьох DOM‑вузлів; дорога підсвітка; парсинг JSON; fuzzy matching у головному потоці).

Виправлення: тримайте DOM малим; обмежте кількість результатів; попередньо обчислюйте підсвітку; перемістіть важку логіку в web worker; дебаунсьте мережеві виклики, але не локальний рендер інпуту.

2) «Стрілки іноді перестають працювати»

Симптом: навігація працює, потім раптом ні; фокус здається втраченим.

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

Виправлення: тримайте фокус в інпуті (модель active‑descendant) або забезпечте, щоб елементи списку були консистентно фокусовані; при відкритті встановлюйте фокус детерміновано; при закритті відновлюйте фокус на тригері.

3) «Esc закриває, але фонова сторінка все одно скролиться»

Симптом: користувач скролить і сторінка позаду рухається; модалка лишається.

Причина: блокування прокрутки тіла не застосоване або некоректно для iOS/overflow контейнерів.

Виправлення: блокування скролу за перевіреним підходом; переконайтесь, що область прокрутки — контейнер результатів. Перевіряйте на мобільному Safari окремо.

4) «Миготить «Немає результатів», поки завантажуються результати»

Симптом: порожній стан показується коротко, потім з’являються результати.

Причина: порожній стан прив’язаний до «довжина масиву результатів === 0» без урахування стану «завантаження»; гонка між запитами.

Виправлення: явні стани: idle, loading, loaded, error. Прив’язуйте кожен відрендерений вид до ID запиту.

5) «Результати виглядають правильно, але відкривається неправильний елемент»

Симптом: підсвітка на елементі A; Enter відкриває елемент B.

Причина: ключі по індексу масиву; список переформатований; індекс вибору вказує на старий порядок.

Виправлення: зберігайте вибір за стабільним ID результату; оновлюйте вибір при зміні результатів; не використовуйте індекс‑базовану прив’язку для активації.

6) «Пошук працював вчора; сьогодні порожньо для деяких користувачів»

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

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

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

7) «Cmd+K перестає працювати в текстових полях»

Симптом: шорткат конфліктує з редагуванням або блокується.

Причина: глобальний обробник клавіш ігнорує контекст цільового елемента або невірно викликає preventDefault.

Виправлення: не тригерте в editable елементах; дозвольте opt‑out; обмежуйте обробку шорткатів консервативно.

8) «Користувачі скрінрідерів не чують, що вибрано»

Симптом: список оновлюється, але немає оголошення; вибір лишається беззвучним.

Причина: відсутні ролі/ARIA‑зв’язки; active descendant не підключений; вибір позначений лише візуально.

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

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

Покроковий план побудови (HTML/CSS‑перший)

  1. Визначте контракт: що можна шукати (документи, команди, люди), які метадані можна показувати і що відбувається на межах прав доступу.
  2. Розмітьте оболонку діалогу: включіть заголовок, інпут, контейнер результатів і футер з підказками. Нехай це буде читабельно без JS‑поведінки.
  3. Реалізуйте правила фокусу: при відкритті фокус на інпуті; при закритті відновити фокус; ловіть фокус всередині модалу; переконайтеся, що Esc завжди закриває.
  4. Реалізуйте навігацію списком: оберіть модель фокусу (active descendant чи переміщення фокусу). Зробіть / детермінованими.
  5. Додайте стани: idle, loading, loaded, empty, error/offline. Зробіть кожен стан візуально відмінним і відредагованим.
  6. Додайте підказки клавіатури: показуйте лише клавіші, які працюють у поточному стані. Адаптуйте модифікатори по платформі.
  7. Гардлайни продуктивності: обмежте показ результатів; обмежте частоту запитів; скасовуйте незавершені запити; кешуйте недавні запити безпечно.
  8. Обсервабіліті: логування request_id і тривалостей; інструментуйте час‑до‑відкриття і час‑до‑результатів; відстежуйте частоту порожніх станів.
  9. Тести доступності: перевірте тільки клавіатурою; потім зі скрінрідером; потім у режимі високої контрастності; потім з вимкненими анімаціями.
  10. План випуску: випускайте за feature flag; канарка; стежте за error budget; розгортайте вперед або назад з наміром.

Чекліст перед запуском (те, що ламається в масштабі)

  • Відкриття модального — менше 100 мс на середньому обладнанні.
  • Навігація результатами без thrash (без стрибків скролу; вибір залишається у полі зору).
  • API витримує бурсти набору без 429‑штормів.
  • Таймаути продукують стан помилки, а не «Немає результатів».
  • Модель прав: ви не витікаєте заголовків об’єктів із обмеженим доступом.
  • Порожній стан пояснює область пошуку і пропонує наступну дію.
  • Підказки коректні на macOS і Windows і не шумлять на мобайлі.
  • Закриття модального скасовує роботу і відновлює фокус надійно.

Чекліст на чергуванні (коли вже зламалось)

  • Перевірте access logs на 429/504 для /api/search.
  • Перевірте CPU і I/O бекенду на насичення.
  • Перевірте, чи UI не відправляє порожні або фон‑запити.
  • Вимкніть feature flag, якщо він створює сплески навантаження.
  • Комунікуйте тимчасовий обхід (навігаційний пошук, окрема сторінка пошуку), поки виправляєте палітру.

FAQ

Чи має Cmd+K бути «командною палітрою» чи «полем пошуку»?

Зробіть і те, і інше, але відокремлюйте типи результатів візуально та семантично. Користувачі писатимуть «create invoice» так само, як «invoice 10492». Не змушуйте їх гадати, в якому вони режимі.

Чи потрібен ARIA combobox, чи можна просто список під інпутом?

Можна використовувати простішу модель textbox + listbox, але все одно треба надати правильні ролі і оголошення вибору. Напівреалізований combobox гірший за правильно реалізований простий підхід.

Скільки результатів показувати?

Почніть з 8–12 видимих рядків і дозвольте прокрутку для решти. Палітра потрібна для швидких дій; показ 50 результатів без віртуалізації — податок на продуктивність і когнітивне навантаження.

Чи має список оновлюватися на кожен натиск клавіші?

Так, але це не означає, що ви маєте викликати бекенд на кожен натиск. Оновляйте UI одразу (стан loading), а потім фетчіть з дебаунсом і скасуванням.

Як обробляти IME/композицію вводу?

Не трактуйте події композиції як остаточні запити. Уникайте відправлення запитів під час композиції; тригеріть пошук коли композиція завершується або отримано committed input event.

Який найкращий текст для порожнього стану?

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

Показувати «недавні пошуки» чи «недавні елементи»?

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

Як не дати палітрі забити бекенд?

Дебаунс на клієнті (малий), скасування запитів, кешування недавніх запитів і серверні захисні обмеження. Далі інструментуйте запити на користувача, щоб помічати регресії.

Чи варто віртуалізувати список результатів?

Тільки якщо ви дійсно показуєте великі списки. Віртуалізація ускладнює реалізацію і може шкодити доступності. Для палітри частіше краще обмежити результати і тримати DOM простим.

Як зробити підказки клавіш платформно‑коректними?

Визначайте платформу в рантаймі і рендерте Cmd проти Ctrl. Також не показуйте підказки з важкими модифікаторами, якщо користувач на сенсорному пристрої.

Висновок: наступні кроки, які можна зробити

Якщо ви хочете Cmd+K палітру, що відчувається як нативний інструмент замість крихкої накладки, зробіть три речі цього тижня:

  1. Зробіть стани явними: idle, loading, loaded, empty, error. Перестаньте закривати таймаути під «Немає результатів».
  2. Аудит списку результатів: поверхневі рядки, стабільні ID, вибір як стан, і без innerHTML‑фокусних хаκів.
  3. Одягніть SRE‑шолом: вимірюйте частоту запитів, хвостову латентність і частоту порожніх станів. Додайте зворотний тиск перш ніж він знадобиться.

Потім випускайте під feature flag. Канаркуйте. Слідкуйте, як за будь‑яким сервісом, що може створювати сплески навантаження. Бо саме це це і є: сервіс із клавіатурою.

Побудовано з операційним ухилом: якщо це неможливо швидко віддебажити, воно ще не готове.

← Попередня
Ubuntu 24.04: DNS-кеші брешуть — очищуйте правильний кеш (і припиніть очищати не той) (випадок №26)
Наступна →
Пошкодження поштових скриньок Dovecot: кроки відновлення з мінімізацією шкоди

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