From cac40b47b73a2fe0c4855ea399aa979f8ff53e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=98=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Tue, 6 Oct 2026 18:20:59 +0300 Subject: [PATCH] docs: expand UI state answers --- src/pages/ui-ux/index.md | 632 +++++++++++++++++++++++++++++++++++---- 1 file changed, 575 insertions(+), 57 deletions(-) diff --git a/src/pages/ui-ux/index.md b/src/pages/ui-ux/index.md index 5162a589..321df3a2 100644 --- a/src/pages/ui-ux/index.md +++ b/src/pages/ui-ux/index.md @@ -733,8 +733,29 @@ UI отражается и в сложном state model frontend. **Полный ответ** -Обычно нужны default, hover, focus, active, disabled, loading, success и error, если компонент участвует в действии. Для -интерактивных элементов важно продумать мышь, клавиатуру и touch. +Набор состояний зависит от роли компонента, но интерактивный control обычно нужно рассматривать как state machine, а не +как один статичный вид. + +Для кнопки или input стоит проверить как минимум: + +- default; +- hover для pointer devices; +- focus/focus-visible для keyboard navigation; +- active/pressed; +- disabled или readonly, если это допустимо; +- loading/submitting; +- success/error, если действие имеет результат. + +Также есть внешние состояния: permission denied, offline, stale data, validation error, selected, expanded, drag state. + +Важно не просто нарисовать состояния, а определить переходы между ними. Например, что произойдет, если пользователь +повторно нажмет кнопку во время loading, уйдет со страницы или получит ошибку после optimistic update. + +Mouse, keyboard и touch дают разные interaction signals. Hover нельзя считать обязательным способом раскрыть критичную +информацию, потому что на touch его нет. + +На интервью сильный ответ: **состояния компонента — это контракт поведения во времени; нужно назвать не только +визуальные варианты, но и переходы, input methods и edge cases**. @@ -750,8 +771,28 @@ Loading state показывает, что данные или действие **Полный ответ** -Loading state показывает, что данные или действие еще выполняются. Он должен объяснять, что происходит, особенно если -ожидание дольше мгновения. +Loading state сообщает, что система приняла запрос и работа продолжается. Его задача — убрать неопределенность и +предотвратить лишние повторные действия. + +Loading бывает разным: + +- initial loading — данных еще нет; +- refresh — старые данные уже есть и обновляются; +- action loading — выполняется конкретная команда; +- progressive loading — части экрана приходят независимо; +- long-running operation — процесс может занять заметное время. + +Эти случаи не стоит показывать одинаково. При refresh часто лучше сохранить старые данные и показать небольшой progress, +чем заменить весь экран spinner. + +Для долгой операции полезно сообщить, что именно происходит, а если можно оценить progress — показать его. Если операция +продолжается в фоне, пользователь должен понимать, можно ли безопасно уйти со страницы. + +Loading state также должен быть доступным: изменение состояния стоит отражать в тексте/ARIA там, где это важно для +assistive technologies. + +На интервью: **loading — это feedback о состоянии операции, а выбор spinner/skeleton/progress зависит от структуры, +длительности и того, есть ли уже полезные данные**. @@ -767,8 +808,28 @@ Empty state показывает, что данных пока нет, но эк **Полный ответ** -Empty state показывает, что данных пока нет, но экран работает корректно. Хороший empty state объясняет причину и часто -предлагает следующий шаг. +Empty state — это валидный результат, а не ошибка загрузки. Он отвечает на два вопроса: **почему здесь пусто** и **что +можно сделать дальше**. + +Причины могут быть разными: + +- пользователь еще ничего не создал; +- фильтр не нашел результатов; +- данных нет из-за прав; +- история появится только после первого действия; +- выбранный период действительно пустой. + +Поэтому generic "No data" часто недостаточно. Для первого запуска полезен onboarding CTA, для filtered empty — сброс +фильтров, а при отсутствии прав CTA создания вообще может быть недоступен. + +Важно отличать initial empty от filtered empty: пользователь должен понимать, что сущности существуют, просто текущий +query их не показывает. + +Empty state не должен притворяться success screen, если данные не загрузились из-за ошибки. Error и empty — разные +семантические состояния. + +На интервью: **хороший empty state объясняет контекст пустоты и дает релевантный recovery/next step, не выдавая failure +за отсутствие данных**. @@ -784,8 +845,30 @@ Error state сообщает, что действие или загрузка н **Полный ответ** -Error state сообщает, что действие или загрузка не завершились успешно. Он должен быть понятным, не обвинять -пользователя и помогать восстановиться. +Error state нужен, когда система не смогла выполнить ожидаемый сценарий. Хорошее сообщение не просто сообщает failure, а +помогает понять последствия и следующий шаг. + +Полезно различать: + +- validation error; +- network/offline/timeout; +- permission error; +- not found; +- conflict; +- server failure; +- partial failure. + +Для пользователя техническая классификация вторична. Важно объяснить, что не получилось, сохранены ли данные и что можно +сделать: исправить поле, повторить, обновить страницу, запросить доступ или обратиться в поддержку. + +Контекст ошибки должен соответствовать масштабу проблемы. Ошибка одной карточки не должна превращать весь dashboard в +error page. + +Нельзя автоматически показывать raw backend message: в нем могут быть технические детали, небезопасные данные или +непонятная терминология. + +На интервью: **error state должен минимизировать неопределенность и стоимость восстановления: проблема, последствия, +следующее действие и сохранение пользовательского контекста**. @@ -801,8 +884,22 @@ Disabled state означает, что элемент сейчас недост **Полный ответ** -Disabled state означает, что элемент сейчас недоступен для взаимодействия. Если причина не очевидна, рядом нужна -подсказка или другой способ понять ограничение. +Disabled state показывает, что действие сейчас недоступно. Но это сильный UX-инструмент: пользователь часто не может +сфокусировать native disabled control и не получает от него объяснение. + +Disabled уместен, когда действие объективно невозможно или повторное выполнение опасно, например во время submit. + +Плохой случай — заранее disabled submit button без подсказки, когда пользователь не понимает, какое условие не +выполнено. Иногда лучше оставить действие доступным и после попытки показать конкретные validation errors. + +Также важно отличать визуальное "серое" состояние от настоящей semantics. Если custom component выглядит disabled, но +все еще кликается или попадает в keyboard flow, contract нарушен. + +Для permission restrictions иногда полезнее показать действие и объяснить недостаток прав, а иногда — скрыть его. Выбор +зависит от того, важно ли пользователю знать, что такая возможность существует. + +На интервью: **disabled state должен иметь понятную причину и корректную semantics; использовать его как универсальный +способ скрыть validation проблемы не стоит**. @@ -818,8 +915,28 @@ Success state подтверждает, что действие выполнен **Полный ответ** -Success state подтверждает, что действие выполнено. Он особенно важен для операций, где результат не виден сразу: -сохранение, отправка формы, копирование, импорт. +Success state закрывает feedback loop: пользователь должен понять, что система приняла действие и результат достигнут. + +Форма успеха зависит от контекста: + +- inline message после сохранения; +- toast для небольшого non-blocking action; +- новый объект в списке; +- переход на result screen; +- изменение label/icon для copy action; +- progress → completed для импорта. + +Не каждое действие требует большого зеленого баннера. Если результат очевиден сам по себе, дополнительный feedback может +создавать шум. + +Для долгих операций важно различать "запрос принят" и "операция завершена". Сообщение "готово" нельзя показывать после +HTTP 202, если backend еще продолжает работу. + +Success state должен быть устойчив к повторному render и navigation: toast не должен быть единственным доказательством +успеха, если пользователь позже должен проверить результат. + +На интервью: **success feedback должен соответствовать реальному моменту завершения операции и масштабу действия, не +создавая ложного подтверждения**. @@ -835,8 +952,25 @@ Spinner не говорит, что именно происходит и ско **Полный ответ** -Spinner не говорит, что именно происходит и сколько ждать. Без контекста пользователь может подумать, что интерфейс -сломался или действие не началось. +Spinner сообщает только "что-то происходит". Он не отвечает, что загружается, можно ли продолжать работу и что делать, +если ожидание затянулось. + +На whole-page spinner пользователь теряет контекст и не видит уже доступные данные. Поэтому при refresh часто лучше +оставить content на месте и показать progress рядом с обновляемой областью. + +Если операция длинная, полезны: + +- понятный label: "Сохраняем отчет"; +- progress, если он действительно измерим; +- возможность cancel, если операция безопасно отменяется; +- сообщение о background processing; +- timeout/retry path. + +Для быстрых локальных действий spinner остается нормальным решением, например внутри кнопки, если одновременно +предотвращен double submit. + +На интервью: **spinner — визуальный индикатор, а loading UX требует контекста, масштаба операции и понятного поведения +при долгом ожидании или сбое**. @@ -852,8 +986,25 @@ Spinner не говорит, что именно происходит и ско **Полный ответ** -Пустой экран выглядит как ошибка или незагруженный интерфейс. Empty state должен объяснить состояние и подсказать, как -получить первые данные. +Без объяснения пользователь не знает, завершилась ли загрузка, нет ли у него доступа или интерфейс просто сломан. + +Хороший empty state обычно содержит: + +- короткое объяснение причины; +- следующий шаг, если он существует; +- CTA, соответствующий правам; +- иногда пример того, что появится после первого действия. + +Например, пустой список проектов для нового пользователя может предложить "Создать проект". Но после поиска правильнее +показать "По запросу ничего не найдено" и предложить изменить или сбросить фильтр. + +Декоративная illustration необязательна. Она не заменяет смысл и действие. + +Важно также не переусердствовать: если пустое состояние очевидно в маленьком select, полноценный onboarding block будет +избыточным. + +На интервью: **empty state должен подтверждать, что система работает нормально, объяснять причину пустоты и помогать +продолжить сценарий**. @@ -869,8 +1020,32 @@ Spinner не говорит, что именно происходит и ско **Полный ответ** -Нужны initial, dirty, valid, invalid, submitting, success, server error и disabled по правам или контексту. Также важно -решить, когда показывать ошибки и как не потерять введенные данные. +Форма — это stateful flow, а не набор inputs. + +Обычно нужно определить: + +- initial/pristine; +- changed/dirty; +- field validation states; +- form-level validation; +- submitting; +- success; +- server-side validation error; +- transport/server error; +- readonly/disabled по permissions; +- unsaved changes при navigation. + +Нужно отдельно решить timing ошибок: показывать их сразу, после blur или после submit. Агрессивная validation на каждом +символе может мешать вводу. + +При server failure введенные пользователем данные обычно стоит сохранить. Ошибки backend нужно map-ить на конкретные +поля, если это возможно, а общую ошибку показывать на уровне формы. + +Submitting state должен предотвращать accidental duplicate request, но не уничтожать возможность читать введенные +данные. + +На интервью: **хороший ответ описывает форму как state machine от initial до success/error и отдельно говорит о +validation timing, сохранении input и server errors**. @@ -886,8 +1061,32 @@ Spinner не говорит, что именно происходит и ско **Полный ответ** -Нужны loading, частичная загрузка, empty, error, filtered empty, pagination, sorting, selected rows и обновление данных. -Для больших таблиц важны стабильные размеры колонок и отсутствие layout shift. +Таблица редко имеет только "есть rows / нет rows". Нужно продумать весь data lifecycle. + +Минимальный набор: + +- initial loading; +- loaded; +- true empty; +- filtered/search empty; +- load error; +- refresh с уже существующими rows; +- sorting/filtering; +- pagination/infinite loading; +- selection; +- row-level action loading/error; +- stale data. + +При refresh часто лучше не удалять старые rows, иначе пользователь теряет контекст и scroll position. + +Ошибку одной строки или secondary column не обязательно поднимать до full-table failure. Partial/degraded state может +сохранить основной сценарий. + +Размеры columns, header и skeleton стоит делать стабильными, чтобы loading не создавал layout shift. Для больших +datasets нужно учитывать virtualization и сохранение selection при обновлении данных. + +На интервью: **таблица — это data state machine; особенно важны различие initial/refresh, true/filtered empty и +локальные ошибки вместо глобального collapse интерфейса**. @@ -903,8 +1102,33 @@ Spinner не говорит, что именно происходит и ско **Полный ответ** -Нужны пустой запрос, typing, loading, results, no results, error и stale results. Хороший поиск показывает, что именно -искалось, и помогает изменить запрос. +Search UX зависит от того, когда и как запускается запрос. + +Нужно учесть: + +- initial state без query; +- typing; +- debounce; +- loading; +- successful results; +- no results; +- error; +- stale results при новом запросе; +- cancelled/outdated request; +- pagination или load more. + +Критичный edge case — race condition: ответ для старого query может прийти позже нового. UI не должен заменить +актуальные results устаревшими. + +При новом query иногда полезно сохранить предыдущие results до прихода новых, но явно показать loading, чтобы stale data +не воспринимались как ответ на новый текст. + +No results должен отображать сам query и предлагать изменить filters/spelling, если это уместно. + +Keyboard navigation и focus особенно важны для autocomplete/search suggestions. + +На интервью: **поиск — это асинхронный state machine с race conditions; хороший ответ упоминает debounce/cancellation, +no-results и stale data**. @@ -920,8 +1144,22 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Disabled элемент нельзя использовать и обычно он исключается из фокуса. Readonly значение нельзя менять, но его можно -прочитать, выделить, скопировать и часто сфокусировать. +Disabled означает, что control недоступен как действие. Native disabled form controls обычно не участвуют в keyboard +focus и не отправляются вместе с form submission. + +Readonly означает другое: значение доступно пользователю, но редактирование запрещено. Для text inputs `readonly` +оставляет возможность focus, selection и copy, а значение участвует в form submission. + +Это UX-различие важно. Например, номер договора, который нельзя изменить, обычно лучше показывать readonly, а не +disabled: пользователь может его прочитать и скопировать. + +Для custom components нельзя ограничиваться серым цветом. Нужны корректные semantics и keyboard behavior. + +Также readonly не применим ко всем native controls одинаково, поэтому иногда лучше использовать plain text presentation +вместо имитации input. + +На интервью: **disabled = действие сейчас недоступно, readonly = данные доступны для чтения, но не для изменения; эти +states отличаются semantics, focus и form behavior**. @@ -937,8 +1175,25 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Пользователь не понимает, что нужно сделать для продолжения. Часто лучше оставить действие доступным и показать -конкретную ошибку после попытки. +Disabled button сообщает "нельзя", но часто не сообщает "почему". + +Проблема усиливается, если условие зависит от нескольких полей, permission или скрытого validation rule. Пользователь +может последовательно проверять экран и все равно не понять, что требуется. + +Кроме того, native disabled control нельзя сфокусировать, поэтому tooltip только на самой disabled кнопке может быть +недоступен keyboard users. + +Варианты лучше: + +- показывать inline validation у проблемного поля; +- выводить helper text с условием; +- оставить submit доступным и после попытки перевести focus к первой ошибке; +- использовать disabled только когда причина очевидна или действие действительно опасно повторять. + +Это не правило "никогда не disable". Во время уже выполняющегося submit блокировка повторной отправки вполне оправдана. + +На интервью: **disabled state должен быть объяснимым; если пользователь не может понять путь к enabled state, интерфейс +создает тупик вместо помощи**. @@ -954,8 +1209,24 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Когда причина недоступности неочевидна или зависит от нескольких условий. Нажатие может стать способом объяснить -пользователю, что именно нужно исправить. +Оставлять action enabled полезно, когда сама попытка — естественный момент validation. + +Типичный пример — submit большой формы. Пользователь нажимает кнопку, после чего интерфейс: + +- показывает ошибки у конкретных полей; +- переводит focus/scroll к первой проблеме; +- сохраняет введенные данные; +- объясняет, что нужно исправить. + +Если кнопку заранее disable-ить, пользователь может не знать, какое поле блокирует продолжение. + +Но есть исключения. Нельзя позволять повторный destructive action, если request уже выполняется. Также action может быть +недоступен по security/permission contract. + +Решение зависит от стоимости попытки. Validation attempt дешевый и информативный; повторная финансовая транзакция — нет. + +На интервью: **enable + validation подходит, когда попытка безопасна и помогает объяснить ошибки; disabled нужен, когда +действие объективно невозможно или повтор опасен**. @@ -971,8 +1242,26 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Сообщение должно назвать проблему простыми словами и предложить действие: исправить поле, повторить попытку, проверить -подключение или обратиться в поддержку. Важно сохранять данные пользователя. +Полезная структура error message: + +1. что не получилось; +2. что это значит для пользователя; +3. что можно сделать сейчас. + +Например вместо "Request failed: 504" лучше: "Не удалось загрузить отчет. Данные не изменены. Повторите попытку." + +Если retry может создать duplicate operation, нельзя предлагать его без idempotency/состояния запроса. + +Важно сохранять пользовательский контекст: input, filters, selected tab, scroll position — все, что не связано напрямую +с ошибкой. + +Для field error сообщение располагают рядом с полем и связывают semantics. Для page-level failure нужен более заметный +state. + +Если самостоятельно восстановиться нельзя, можно дать support path и correlation/reference ID, но не dump stack trace. + +На интервью: **error UX оценивают по recovery path: сообщение должно быть конкретным, сохранять контекст и предлагать +только безопасное следующее действие**. @@ -988,8 +1277,25 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Он делает интерфейс быстрее на ощущение, потому что пользователь сразу видит результат действия. Это хорошо работает для -частых и обычно успешных операций. +При optimistic update UI применяет ожидаемый результат до подтверждения server. + +Например после "лайка" icon может сразу стать active, а request уйти в фоне. Пользователь не ждет network round trip для +очевидного feedback. + +Подход хорошо работает, когда: + +- операция часто успешна; +- результат легко откатить; +- conflict редок; +- цена временно неверного UI невысока. + +Можно сразу обновить local cache, а после ответа server подтвердить или скорректировать state. + +Для high-risk операций — платеж, удаление критичных данных, изменение прав — optimistic UI может быть опасным, если +показывает success до реального commit. + +На интервью: **optimistic update уменьшает perceived latency, но оправдан только когда rollback понятен и временно +несогласованное состояние не создает серьезного риска**. @@ -1005,8 +1311,26 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Если сервер отклонит действие, нужно откатить состояние и понятно объяснить проблему. Без аккуратной синхронизации можно -показать пользователю данные, которых на самом деле нет. +Главный риск — UI временно показывает результат, которого server не подтвердил. + +Проблемы: + +- request rejected; +- concurrent update от другого клиента; +- server нормализовал значение иначе; +- пользователь сделал следующее действие на основе неподтвержденного state; +- rollback конфликтует с более новым local change; +- offline queue долго не синхронизируется. + +Поэтому optimistic state полезно связывать с operation ID/version и продумывать rollback/reconciliation. + +Иногда лучше не "откатывать молча", а показать failed state конкретного элемента и дать retry. Иначе пользователь +увидит, как объект исчез и вернулся без объяснения. + +Для irreversible/high-value операций лучше дождаться server confirmation. + +На интервью: **optimistic UI требует стратегии rollback и reconciliation; проблема не только в ошибке запроса, но и в +гонках между local и server state**. @@ -1022,8 +1346,21 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Лучше показать доступные части интерфейса и явно отметить недоступные блоки. Полный экран ошибки нужен только когда -основной сценарий действительно невозможен. +Если разные части экрана загружаются независимо, failure secondary блока не должен автоматически уничтожать весь +полезный content. + +Например profile загружен, а recommendations API упал. Можно показать profile и локальный error/retry в recommendations. + +Для этого архитектура UI должна поддерживать независимые states, а не один глобальный `isLoading/isError` на всю page. + +При partial data важно не скрывать, что информация неполная. Пользователь должен понимать, какие части актуальны, а +какие недоступны или stale. + +Однако если отсутствуют данные, без которых основной action становится опасным, degraded mode может быть неправильным — +лучше заблокировать конкретное действие или весь flow. + +На интервью: **partial failure нужно локализовать по boundary; сохранять доступный content можно только если это не +создает ложное ощущение полноты и не делает основной сценарий небезопасным**. @@ -1039,8 +1376,29 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Нужно показывать прогресс, сохранять структуру экрана, кешировать предыдущие данные и не блокировать все приложение. Для -долгих операций полезны статусы, фоновые обновления и понятный retry. +Сначала стоит разделить read и command operations. + +Для медленного чтения: + +- показывать cached/previous data, если это безопасно; +- обновлять их в фоне; +- сохранять layout и scroll; +- использовать skeleton только при initial load. + +Для медленной команды: + +- немедленно подтвердить, что action принят; +- заблокировать только конфликтующие повторные действия; +- показывать status/progress; +- разрешить продолжать работу, если операция может идти в фоне. + +Timeout не всегда означает, что server не выполнил команду. Поэтому blind retry опасен для non-idempotent operations. + +Если backend поддерживает async job, UI может показывать queued/running/completed/failed state вместо держания request +открытым. + +На интервью: **медленный API — не повод блокировать весь интерфейс; нужно сохранять полезный контекст и проектировать +отдельно perceived latency, progress и безопасность retry**. @@ -1056,8 +1414,27 @@ Disabled элемент нельзя использовать и обычно о **Полный ответ** -Интерфейс должен различать offline, timeout, server error и stale data. Пользователь должен понимать, какие данные уже -сохранены, что можно повторить и что требует подключения. +"Network error" недостаточно: разные failure требуют разного поведения. + +Полезно различать: + +- browser offline; +- request timeout; +- DNS/transport failure; +- server unavailable; +- request rejected приложением; +- stale cached data. + +Для read operations можно показывать last known data с отметкой, что они могут быть устаревшими. + +Для write operations особенно важно не обещать сохранение, пока оно не подтверждено. Если продукт поддерживает offline +queue, пользователь должен видеть pending/synced/failed state. + +Retry стоит делать автоматически только там, где операция идемпотентна и backoff не создает лишнюю нагрузку. Для command +request повтор может породить дубликат. + +На интервью: **unstable network требует явного различия confirmed и pending data; stale cache и retry полезны, если UI +честно показывает состояние синхронизации**. @@ -1073,8 +1450,25 @@ Error state означает, что сценарий не выполнен. Deg **Полный ответ** -Error state означает, что сценарий не выполнен. Degraded state означает, что продукт работает частично: например, -показаны кешированные данные без свежих рекомендаций. +В error state нужная функция недоступна: пользователь не может завершить ожидаемый сценарий. + +Degraded state означает, что часть качества или возможностей потеряна, но основной сценарий остается полезным. + +Примеры degraded mode: + +- показываются cached данные вместо свежих; +- recommendations временно отсутствуют; +- image previews не загрузились, но документы доступны; +- realtime updates не работают, но manual refresh доступен. + +Degraded UI должен явно обозначать ограничение, особенно если freshness или полнота данных важны для решения +пользователя. + +Не стоит использовать degraded mode, если missing data критичны. Например финансовое действие на заведомо устаревшем +balance может быть небезопасно. + +На интервью: **degraded state сохраняет основной value при частичном failure, но обязан показать потерянную capability и +не выдавать неполные данные за полностью актуальные**. @@ -1090,8 +1484,29 @@ Skeleton полезен, когда известна структура буду **Полный ответ** -Skeleton полезен, когда известна структура будущего контента и важно сохранить layout. Spinner подходит для коротких -операций или небольших областей, где структура заранее неясна. +Skeleton имитирует geometry будущего content, поэтому хорошо работает для initial load predictable cards/lists/profile. + +Плюсы: + +- сохраняет layout; +- показывает, где появится content; +- может улучшить perceived performance. + +Spinner лучше для: + +- небольшого action; +- неопределенной структуры; +- локального refresh; +- short operation. + +Если данные уже есть, часто лучше не использовать ни skeleton, ни full spinner, а оставить content и показать subtle +refresh indicator. + +Skeleton не должен копировать каждую строку интерфейса с pixel precision. Его задача — передать structure, а не создать +еще одну сложную версию UI. + +На интервью: **выбор зависит от контекста: skeleton — structural placeholder initial load, spinner — generic progress +indicator; при refresh существующий content обычно ценнее обоих**. @@ -1107,8 +1522,24 @@ Skeleton полезен, когда известна структура буду **Полный ответ** -Если skeleton не совпадает с реальным контентом, он вызывает layout shift и обманывает ожидания. Для очень долгой -загрузки он может выглядеть как зависший интерфейс. +Skeleton создает ожидание структуры. Если после загрузки geometry резко меняется, пользователь получает дополнительный +layout shift вместо пользы. + +Другие проблемы: + +- слишком активная shimmer animation утомляет; +- skeleton скрывает, что ожидание уже необычно долго; +- сложный skeleton увеличивает CSS/render cost; +- accessibility tree может получить декоративный шум; +- при refresh он стирает уже полезные данные. + +Для long-running load полезно дополнить placeholder осмысленным status text или перейти к error/slow-state после +разумного threshold. + +Decorative skeleton elements стоит скрывать от assistive technologies. + +На интервью: **skeleton — не универсальный "современный loader"; он полезен только при стабильной предсказуемой +структуре и не должен заменять реальный feedback при долгом ожидании**. @@ -1124,8 +1555,24 @@ Skeleton полезен, когда известна структура буду **Полный ответ** -Нужно резервировать место под контент, задавать размеры медиа, таблиц и карточек, а также не вставлять поздно крупные -элементы перед уже показанным контентом. Это улучшает и UX, и Core Web Vitals. +Главный принцип — browser должен знать geometry как можно раньше. + +Практические приемы: + +- задавать `width`/`height` images или `aspect-ratio`; +- резервировать высоту media/banner; +- использовать skeleton с близкой geometry; +- не вставлять поздний content над текущим viewport без необходимости; +- стабилизировать table/column dimensions; +- аккуратно работать с custom fonts и fallback metrics. + +Если content действительно variable, не нужно искусственно фиксировать огромную height. Лучше резервировать ожидаемую +структуру и позволять естественный layout после загрузки. + +При client hydration важно избегать server/client differences, которые перестраивают layout после first paint. + +На интервью: **layout stability достигается ранним знанием размеров и сохранением структуры между loading и loaded +state, а не просто добавлением placeholder**. @@ -1136,13 +1583,32 @@ Skeleton полезен, когда известна структура буду **Короткий ответ** -Perceived performance - это ощущение скорости, а не только реальное время ответа. Понятный loading, стабильный layout и +Perceived performance — это ощущение скорости, а не только реальное время ответа. Понятный loading, стабильный layout и ранний показ полезного контента делают ожидание короче субъективно. **Полный ответ** -Perceived performance - это ощущение скорости, а не только реальное время ответа. Понятный loading, стабильный layout и -ранний показ полезного контента делают ожидание короче субъективно. +Два интерфейса с одинаковым network time могут восприниматься по-разному. + +UX кажется быстрее, если: + +- пользователь сразу получает feedback; +- shell/layout появляется быстро; +- полезный content показывается progressively; +- old data не исчезают при refresh; +- интерфейс остается responsive; +- нет скачков layout. + +Но perceived performance нельзя улучшать обманом. Fake progress, мгновенный success до server confirmation или скрытие +реального failure ухудшают доверие. + +Лучшее решение — сочетать реальную performance optimization с правильной presentation ожидания. + +Например cached list + background refresh обычно ощущается быстрее, чем каждый раз full-page spinner, даже при +одинаковом API latency. + +На интервью: **loading UX управляет неопределенностью и ощущением progress, но не заменяет реальную оптимизацию latency +и не должен показывать ложное состояние**. @@ -1158,8 +1624,25 @@ Retry должен быть видимым, безопасным и не при **Полный ответ** -Retry должен быть видимым, безопасным и не приводить к дублированию операции. Хорошо, когда сообщение объясняет причину -сбоя и сохраняет контекст пользователя. +Перед retry нужно понять, известен ли результат первой попытки. + +Для GET-like read request повтор обычно безопасен. Для payment/create command timeout может означать, что server уже +выполнил операцию, просто client не получил ответ. + +Поэтому надежный retry для commands часто требует: + +- idempotency key; +- operation ID/status endpoint; +- deduplication на backend; +- сохранение pending state на frontend. + +UI должен сохранять input/context и показывать retry рядом с failed областью, а не заставлять повторять весь flow. + +Автоматический retry уместен для transient read failures с backoff. Бесконечные попытки могут скрыть проблему и зря +нагружать server. + +На интервью: **retry — это не просто кнопка "Повторить"; нужно доказать, что повтор безопасен и пользователь не получит +дубликат или потерю контекста**. @@ -1175,8 +1658,25 @@ Retry должен быть видимым, безопасным и не при **Полный ответ** -Нужно менять состояние конкретного действия: loading label, disabled от повторного запуска, progress или inline status. -Важно не блокировать больше интерфейса, чем требуется. +Feedback лучше располагать рядом с источником действия. + +Для кнопки это может быть: + +- label "Сохраняем..."; +- spinner/icon; +- `aria-busy`/status announcement, если нужно; +- временная блокировка именно конфликтующего action. + +Если операция длительная, локальный indicator можно дополнить progress/status area. + +Не стоит ставить global overlay из-за сохранения одной карточки: пользователь теряет возможность читать или работать с +независимыми частями экрана. + +После completion loading должен перейти в понятный success/error state, иначе пользователь не знает, закончилось ли +действие. + +На интервью: **feedback должен быть локальным, своевременным и соответствовать scope операции; loading state также +защищает от повторного action, но не обязан блокировать всю страницу**. @@ -1192,8 +1692,26 @@ backend все равно нужна идемпотентность или за **Полный ответ** -На frontend нужно блокировать повторную отправку, показывать submitting state и игнорировать лишние клики до ответа. На -backend все равно нужна идемпотентность или защита от дублей. +Frontend защита улучшает UX, но не является гарантией целостности. + +На client: + +- после submit перевести форму в submitting state; +- не запускать второй concurrent request; +- сохранить данные формы; +- показать progress; +- вернуть возможность повторить только после понятного failure. + +Но duplicate request может появиться не только из-за двойного клика: retry proxy, reconnect, refresh или два client +instances тоже возможны. + +Поэтому для операций с side effects backend должен иметь собственную защиту: idempotency key, unique constraint, +operation token или другой domain mechanism. + +После timeout особенно опасно просто снова отправить create/payment без проверки результата первой попытки. + +На интервью: **double submit решается двумя слоями: frontend предотвращает случайный повтор и дает feedback, backend +гарантирует idempotency/deduplication**.