9 нативных API браузера вместо npm-пакетов

Если вы привыкли ставить npm-пакет под каждую мелочь — вот 9 вещей, под которые давно существует нативный API браузера. Кода меньше, багов меньше, бандл легче.

Обложка: 9 нативных API браузера вместо npm-пакетов

Если вы привыкли ставить npm-пакет под каждую задачу — вот девять вещей, под которые давно существует нативный API браузера. Кода меньше, багов меньше, бандл легче. Статья польской разработчицы Sylwia Łask собрала самые показательные примеры — перевели и адаптировали для русскоязычного читателя.

Что внутри
  • requestIdleCallback — запустить фоновую задачу, когда браузер простаивает
  • :focus-within — стилизовать родителя, внутри которого есть элемент в фокусе
  • navigator.onLine + события offline/online — детектировать пропажу интернета
  • requestAnimationFrame — плавная анимация без рывков
  • Container queries — адаптив относительно размеров контейнера, а не viewport
  • crypto.getRandomValues — криптографически стойкие случайные ID без коллизий
  • <dialog> — нативный модал с доступностью из коробки
  • Web Speech API — распознавание речи без библиотек (только Chromium)
  • @supports — CSS feature detection без костылей

1. «Запустим это потом» → requestIdleCallback

Хотите собирать аналитику, предзагружать данные или генерировать что-то в фоне, не конкурируя с рендером 200 компонентов? requestIdleCallback запускает ваш код в те моменты, когда браузер простаивает. На первый взгляд это кажется узкоспециальной фичей, но на практике кейсов много: сбор аналитики о поведении пользователя, несрочная фоновая обработка картинок, предварительная подготовка данных, которые пригодятся позже.

			function trackUserScrolling() {
  console.log('User scrolled. This changes everything.');
}

if ('requestIdleCallback' in window) {
  requestIdleCallback(trackUserScrolling);
} else {
  setTimeout(trackUserScrolling, 0);
}
		

Поддержка: современные браузеры. В Safari исторически отсутствовал, так что fallback на setTimeout всё ещё полезен.

2. «Почему мой input не подсвечивается?» → :focus-within

Стилизовать элемент с фокусом — просто. А как стилизовать родителя, внутри которого какой-то элемент получил фокус, — задача, которую обычно решают сорока строками JavaScript со слушателями focus и blur. Всё это не нужно: :focus-within делает то же самое одной CSS-строкой.

			.form-field {
  border: 1px solid #ccc;
  padding: 12px;
}

.form-field:focus-within {
  border-color: hotpink;
}
		
			<div class="form-field">
  <input placeholder="Type something meaningful..." />
</div>
		

Поддержка: везде, где это хоть сколько-нибудь важно.

Вечная боль любого PWA — что делать, когда у пользователя пропал интернет (он уехал в лес или зашёл в лифт). Можно писать сложные if-ы — а можно просто слушать события offline и online. На offline складываем данные в IndexedDB, на online отправляем на сервер.

			window.addEventListener('offline', () => {
  alert('You are offline. Time to panic.');
});

window.addEventListener('online', () => {
  alert("You're back. Panic cancelled.");
});
		

Поддержка: широкая. Одна оговорка: «онлайн» не равно «ваш бэкенд доступен». Это проверка уровня сетевого соединения, а не доступности конкретного сервиса.

4. «Плавная анимация, но проклятая» → requestAnimationFrame

Хотите, чтобы анимация не дёргалась на слабых ноутбуках, а батарея садилась медленнее? Привычка «60 fps = setInterval каждые 16 мс» — плохая идея, и вот почему. Классика, которую все видели:

			setInterval(() => {
  element.style.left = Math.random() * 100 + 'px';
}, 16);
		

Интуитивно понятно, что это плохая идея. Лагает. К счастью, есть requestAnimationFrame — он синхронизирован с циклом перерисовки браузера, поэтому анимация действительно плавная.

			function animate() {
  element.style.transform = `translateX(${Date.now() % 300}px)`;
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
		

Поддержка: везде.

5. «Карточка должна адаптироваться, но только здесь» → container queries

Одну и ту же карточку можно положить в узкий сайдбар, в основную ленту или в лайтбокс — и чтобы она корректно подстраивалась под каждое место без знания о том, на каком она экране. Раньше media queries были привязаны к размеру viewport, то есть «ко всей странице». Container queries позволяют применять стили в зависимости от размера конкретного контейнера. Компонент становится самодостаточным: куда положили — под то и подстроился.

			.card-wrapper {
  container-type: inline-size;
}

.card {
  display: grid;
}

@container (min-width: 400px) {
  .card {
    grid-template-columns: 1fr 2fr;
  }
}
		

Поддержка: современные браузеры. Если целитесь в старые — добавьте fallback через обычные media queries.

6. «Случайный ID, что может пойти не так?» → crypto.getRandomValues

Именно так рождаются баги:

			const id = Math.random().toString(36).slice(2);
		

Выглядит как «достаточно случайная» криптография с AliExpress — и работает, пока не перестаёт. Во-первых, всё зависит от реализации движка, мы не знаем, что происходит под капотом. Во-вторых, определённые паттерны вполне возможны, а при большом количестве ID вы фактически напрашиваетесь на коллизии.

К счастью, есть нативное решение. Не серебряная пуля, но crypto.getRandomValues заметно лучше: больше энтропии, нет странных паттернов, вероятность коллизий резко снижается. Браузер просто делает это правильно. А если вам нужен не произвольный набор байтов, а именно UUID, в современных браузерах есть ещё короче: crypto.randomUUID() выдаёт готовый UUID v4 одной строкой — тоже криптографически стойко и без ручной возни с байтами.

			const bytes = new Uint8Array(8);
crypto.getRandomValues(bytes);

const id = Array.from(bytes)
  .map(b => b.toString(16).padStart(2, '0'))
  .join('');

console.log('Secure-ish ID:', id);
		

Поддержка: широкая.

7. «Нам нужен модал» → dialog

Больше не нужно ставить 12-килобайтную библиотеку ради модального окна, которое так любят пользователи. Нативный <dialog> даёт клавиатурную навигацию, фокус-трап и корректную работу со скринридерами прямо из коробки — всё то, что в самописных модалах обычно забывают или делают криво.

			<dialog id="modal">
  <p>Are you sure you want to deploy on Friday?</p>
  <button onclick="modal.close()">Cancel</button>
  <button onclick="alert('Good luck')">Deploy</button>
</dialog>

<button onclick="modal.showModal()">Open modal</button>
		

Поддержка: современные браузеры.

8. «Голосовой ввод был бы крутой фичей» → Web Speech API

Собирались ставить transformers.js, потому что понадобилось распознавание речи? У браузера для этого уже есть Web Speech API. Chromium-браузеры поддерживают его напрямую, Safari — через префиксную версию webkitSpeechRecognition (код ниже как раз это учитывает), в Firefox поддержки нет. Для демо и ассистивных фич — отлично, в проде лучше иметь запасной план на случай Firefox.

			const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

if (SpeechRecognition) {
  const recognition = new SpeechRecognition();
  recognition.onresult = e => {
    console.log('You said:', e.results[0][0].transcript);
  };
  recognition.start();
}
		

Поддержка: Chromium и Safari (через webkit-префикс), в Firefox до сих пор нет.

9. «Не сломает ли это CSS?» → @supports

Хотите выкатить backdrop-filter или :has() и при этом не сломать вёрстку в браузере, где фича ещё не поддерживается? Оборачиваем в @supports — и спокойны: браузер, который знает фичу, получит красивую версию, остальные — дефолтный fallback.

			.card {
  background: white;
}

@supports (backdrop-filter: blur(10px)) {
  .card {
    backdrop-filter: blur(10px);
    background: rgba(255, 255, 255, 0.6);
  }
}
		

Поддержка: очень хорошая.

Когда всё-таки нужна библиотека

Библиотеки — это отлично, и иногда они действительно необходимы. Но иногда вы ставите зависимость на то, что браузер решил годы назад. Перед npm install полезно спросить себя (или поискать в MDN): «А браузер не умнее меня в этом вопросе?» Иногда ответ — да. И это нормально.

Источник: Sylwia Łask — 9 things you're overengineering: the browser already solved them.