На кухонной панели с семидюймовым экраном открывается сайт доставки продуктов, и половина кнопок не помещается в видимую область. Пользователь тычет пальцем в элемент размером с горошину, промахивается и уходит за телефоном. Разбираемся, чем такие экраны отличаются и как строить работающий интерфейс. Проблема в том, что веб-разработка десятилетиями ориентировалась на мышь, клавиатуру и крупные мониторы. IoT-панели ломают эти привычки: экран бывает любым, ввод идет пальцем, перчаткой, голосом или пультом, а процессор варьируется от бюджетного микроконтроллера до полноценного x86. Универсального рецепта нет, но есть принципы, работающие на любом железе.
Устройства интернета вещей отличаются от телефонов по четырём параметрам
Первое отличие это физический размер экрана. Панели от 2 до 15 дюймов не тот диапазон, к которому привыкли мобильные разработчики: на малом экране нет места второстепенным элементам, а даже на 15 дюймах плотность пикселей бывает низкой, и текст 16 пикселей читается плохо с полутора метров.
Второе отличие это расстояние до пользователя: телефон держат в руках, панель висит на стене или стоит на столе, поэтому базовый кегль заметно выше, чем на телефоне. Минимальный кегль для настенной панели это 20 пикселей, для настольной 18, точное значение зависит от расстояния глаз до экрана.
Третье отличие это способ ввода: часть устройств управляется касанием, часть кнопками, часть голосом, бывают панели без экрана с парой индикаторов и одной кнопкой. На сенсоре нет hover-состояния, правого клика и клавиатурных сокращений, всё, что на десктопе делается наведением, должно работать тапом или долгим нажатием.
Четвёртое отличие это вычислительные ресурсы: слабый процессор, мало памяти, медленное соединение. Тяжелый сценарий с десятком библиотек превратит загрузку в пытку, а на Cortex-A7 с 512 МБ RAM даже React-хранилище тормозит без ограничения перерисовок.
Из четырех пунктов вытекает пятый, неочевидный: такой интерфейс редко бывает основным местом работы, к нему подходят на минуту, поэтому глубина навигации минимальна и любой сценарий длиннее двух тапов теряет пользователя.
Основой служит адаптивная верстка с точками перехода по возможностям
Адаптивность начинается с метатега viewport:
<meta name="viewport" content="width=device-width, initial-scale=1" />
Дальше точки перехода: в отличие от привычной верстки, здесь ориентируемся на возможности устройства, а не только на ширину.
Медиазапросы проверяют ориентацию, разрешение и схему ввода, которая позволяет отличить палец от мыши и клавиатуру.
:root { font-size: 20px; --touch-target: 64px; }
@media (orientation: portrait) and (max-width: 800px) { .layout { grid-template-columns: 1fr; } }
@media (orientation: landscape) and (min-width: 801px) { .layout { grid-template-columns: 320px 1fr; } }
@media (pointer: coarse) { .control { min-height: var(--touch-target); min-width: var(--touch-target); } }
Размер базового шрифта 20 пикселей выглядит вызывающе для веб-разработчика, привыкшего к 16, но для настенной панели это норма: текст читается с полутора метров. Если панель выше человеческого роста, базовый кегль лучше поднять до 24 пикселей: затраты на верстку минимальны, а удобство вырастает многократно.
Второй прием это единицы измерения: абсолютные пиксели привязывают интерфейс к устройству, а относительные позволяют менять масштаб целиком, не переписывая верстку, что удобно, ведь одинаковые разрешения встречаются с разной плотностью пикселей. rem для шрифтов, em для отступов внутри компонентов, vw/vh для полноэкранных контейнеров.
Третий прием это контейнерные запросы: компонент подстраивается под ширину родительского блока, а не всего окна, что точнее для панелей с блочной раскладкой, где один виджет занимает разные части экрана. @container (min-width: 400px) { .widget { grid-template-columns: 1fr 1fr; } } работает надежнее медиазапросов, когда виджет попадает в сайдбар или основную колонку.
Сенсорное управление требует крупных целей и щедрых отступов
Пальцем в мелкую кнопку попасть трудно, на вертикальной панели еще и неудобно. Минимальный размер цели для касания это около 44 пикселей по меньшей стороне, для панели значение стоит увеличить до 64, а на промышленных панелях в перчатках таргет начинается с 72 пикселей, иначе оператор промахивается даже в спокойной обстановке.
Второе требование это расстояние между целями: соседние кнопки вплотную приводят к случайным нажатиям, отступ в 8 пикселей нижняя граница, комфортная начинается с 12, а для критических действий вроде запуска оборудования или оплаты отступ увеличивают до 24 пикселей и добавляют модальное подтверждение.
Третье это визуальная обратная связь: на телефоне палец закрывает место нажатия и пользователь ориентируется на изменение цвета, а на панели экран виден целиком, отклик должен быть заметным, от изменения фона до легкого увеличения и рамки. Активное состояние срабатывает мгновенно, задержка даже в 100 мс создает ощущение зависания.
Четвертое это жесты: прокрутка, масштабирование двумя пальцами и перетаскивание на панелях часто неудобны, лучше строить интерфейс так, чтобы действия выполнялись одиночными нажатиями, а неизбежный жест вроде свайпа для удаления дублировать кнопкой, доступность важнее красоты.
.button { min-height: 64px; min-width: 64px; padding: 12px 20px; font-size: 1.1rem; border-radius: 12px; border: 2px solid transparent; transition: background-color 120ms ease, transform 80ms ease; }
.button:active { transform: scale(0.97); background-color: #dbeafe; }
.button:focus-visible { border-color: #1f6feb; outline: none; }
Фокус по клавиатуре на панелях тоже нужен: часть устройств управляется пультом, часть имеет подключенную клавиатуру, и игнорирование фокуса делает такие устройства бесполезными.
Ограниченные ресурсы диктуют строгую экономию
Слабое устройство не прощает расточительности. Разберем, где теряется производительность.
Первое это загрузка. Большая библиотека ради одной кнопки не оправдана: пишите на стандартных средствах браузера, а когда bundle analyzer покажет, что lodash весит 70 КБ, а нужна одна функция, пишите свою или используйте esbuild с tree-shaking. Код, который не выполняется, не должен попадать в бандл.
Второе это анимации. Плавность обеспечивается аппаратным ускорением, но не все эффекты его получают: свойства, вызывающие пересчет разметки, обходятся дорого. Анимируйте transform и opacity, друзей GPU, а не width/height/margin, его врагов. will-change: transform подсказывает браузеру заранее выделить слой.
.panel-enter { transform: translateY(8px); opacity: 0; }
.panel-enter-active { transition: transform 160ms ease, opacity 160ms ease; }
.panel-enter-to { transform: translateY(0); opacity: 1; }
Третье это постоянные обновления: живые часы, счетчики и таймеры перерисовываются каждую секунду, на слабом процессоре это заметно. Обновляйте только видимые элементы, при уходе со страницы останавливайте обновление, а IntersectionObserver для проверки видимости перед запуском requestAnimationFrame сэкономит кучу ресурсов.
Четвертое это изображения: панель не нуждается в фотографиях высокого разрешения, правильный формат и точные размеры экономят трафик и память. WebP с качеством 75 визуально идентичен JPEG 90 при половине веса, srcset с вариантами под разные DPI отдает каждому устройству его разрешение.
Пятое это шрифты: несколько начертаний с полным набором символов утяжеляют загрузку, для панели достаточно одного начертания и подмножества символов. unicode-range в @font-face заставит браузер качать только кириллицу, а variable font с одной осью weight заменяет 4-6 отдельных файлов.
Офлайновая работа и идентификация
Устройства интернета вещей часто стоят в местах с плохим сигналом: подвал, цех, склад. В метро, подвалах торговых центров и на заводах с металлическими стенами Wi-Fi теряется регулярно, а 4G может не пробиваться, поэтому офлайн это не исключение, а норма, и интерфейс обязан переживать пропадание связи.
Сервис-воркер кэширует оболочку приложения и отдает ее даже без сети, этого достаточно, чтобы интерфейс открывался и показывал последние известные данные. Стратегия stale-while-revalidate отдает кэш сразу и тянет свежие данные в фоне, пользователь не ждет.
const CACHE = "panel-v3";
const SHELL = ["/", "/styles.css", "/app.js", "/offline.html"];
self.addEventListener("install", (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)));
});
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
event.respondWith(
caches.match(event.request).then((cached) => {
const network = fetch(event.request)
.then((response) => {
const copy = response.clone();
caches.open(CACHE).then((cache) => cache.put(event.request, copy));
return response;
})
.catch(() => cached || caches.match("/offline.html"));
return cached || network;
}),
);
});
Такая стратегия отдает сохраненное сразу, пользователь видит мгновенный отклик. Background Sync API (доступен в Chrome/Edge) позволяет отложить отправку формы до появления сети: пользователь нажал "Отправить", закрыл панель, через час сеть появилась, запрос улетел сам.
Второй элемент это локальное хранилище: изменения без сети складываются в очередь и отправляются при восстановлении соединения, а при внезапном отключении питания выживет IndexedDB с автосохранением каждые 500 мс.
Третий элемент это индикация состояния: пользователь обязан видеть, что связи нет и данные могут быть устаревшими, молчаливое поведение хуже явного предупреждения. Индикатор в углу экрана: зеленый - синхронизировано, желтый - есть неотправленное, красный - офлайн давно. Текст под иконкой: "Данные на 14:32" или "Ожидает отправки 3 действия".
Идентификация бывает трех видов: адрес страницы с параметром (легко подделать), сохраненная настройка в локальном хранилище (устройство запоминает роль при первом запуске), учетная запись с привязкой устройства (самый надежный вариант). Device ID в защищенном куке + привязка к MAC-адресу на сервере исключает подмену.
Удаленное обновление: приложение само проверяет новую версию и перезагружается ночью, а принудительная перезагрузка во время работы раздражает, лучше позволить отложить обновление до конца смены. Роллинг-апдейт: 10% панелей получают обновление, 30 минут мониторинга ошибок, потом следующая волна, и если crash-rate не вырос, катим на все.
Ошибки: 1) перенос сайта без переделки - мелкий текст, глубокая навигация; 2) экономия на индикации - зависание; 3) подсказки при наведении - на сенсоре не работают; 4) длинные формы - мучение, сокращать до выбора; 5) выгорание экрана - смещение элементов, режим гашения; 6) нет перезапуска - кнопка в интерфейсе или на корпусе.
Все дешевле учесть на этапе макета, чем исправлять на установленном устройстве: проектирование начинается с наблюдения за работой людей рядом с панелью.
Заключительная проверка перед сдачей экономит нервы
Перед выпуском пройтись по списку: базовый размер текста читается с рабочего расстояния, цель касания не меньше 64 пикселей, сеть отключена - интерфейс работает, сутки работы - потребление памяти проверено, поворот - верстка перестроилась, клавиатура - сценарий без мыши. Эмуляция в браузере лишь приблизительна, настоящий экзамен проходит на железе: Device farm (BrowserStack, Firebase Test Lab) прогоняет тесты на десятках устройств параллельно.
Шесть проверок - час, предотвращают жалоб. Исправление на объекте стоит на порядок дороже. Чек-лист в репозитории (CHECKLIST.md) обязателен к заполнению перед каждым релизом - CI не пропустит пустой.
Интерфейс для IoT-панелей это упражнение в ограничениях. Нет места украшениям, каждое решение проверяется реальностью: видно ли с расстояния, попадает ли палец, открывается ли при плохой связи. Кто проектирует с оглядкой на эти вопросы, получает панель, к которой подходят без раздражения, это единственный измеримый признак хорошего интерфейса. Кто один раз искал баг на установленной в другом городе панели, запомнит: ограничения не препятствие, а карта клада.