[Перевод] 10 браузерных API, заменяющих библиотеки, которые я раньше все время устанавливал

10 браузерных API, заменяющих пакеты NPM и плагины jQuery: dialog, popover, переходы отображения (view transitions), structuredClone, groupBy и Temporal с заметками о поддержке.
❯ 1. IntersectionObserver
На протяжении многих лет “ленивая” (lazy) загрузка изображений означала установку lazysizes или lozad. lazysizes “слушает” события прокрутки, измеряет элементы с помощью getBoundingClientRect() при каждом таком событии и меняет атрибут data-src на src, когда изображение приближается к области просмотра. Это работает, и код выполняется на каждом кадре (frame) прокрутки. lozad появилась позже и является тонкой оберткой над API, рассматриваемым в этом разделе.
Intersection Observer делегирует эту работу браузеру. Вы описываете, какие элементы вас интересуют, и браузер выполняет соответствующий код, когда они входят или покидают область просмотра. Никаких обработчиков прокрутки, никаких вычислений макета в вашем коде.
Шаблон ленивой загрузки становится таким:
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
})
document.querySelectorAll('img[data-src]').forEach((img) => {
observer.observe(img)
})
Для обычных изображений даже этого не требуется. Для их ленивой загрузки существует атрибут loading="lazy". Наблюдатель (observer) нужен для вещей, с которыми этот атрибут не справляется: бесконечная прокрутка, запуск анимации, когда раздел становится видимым, отправка аналитики для частей страницы, которые просматривают люди. Страница демо моей библиотеки tte.js использует наблюдателя для перезапуска анимации текста, когда раздел попадает в область просмотра.
Другие наблюдатели, ResizeObserver и MutationObserver, заменяют плагины обработки изменения размеров элемента и модификации DOM похожим образом.
Нюанс: функция обратного вызова запускается сразу после observe() с текущим состоянием элемента. Если изображение находится за пределами экрана, вы получите сущность (entry) с isIntersecting: false до того, как пользователь начнет прокрутку. Вот для чего нужен continue в примере выше.
Поддержка: все основные браузеры с 2021 года.
❯ 2. Dialog
Модальное окно раньше представляло собой div, немного CSS для выделения на странице и библиотеку, вроде micromodal, или диалоговый виджет jQuery для вещей, которые никто не хочет писать: захват фокуса внутри блока (box), закрытие при нажатии Esc, блокировка кликов на странице за пределами блока и размещение блока поверх всего остального.
Элемент dialog делает все это автоматически:
<dialog id="newsletter">
<form method="dialog">
<p>Get the JavaScript Handbook by email.</p>
<button>Close</button>
</form>
</dialog>
<button id="open">Subscribe</button>
<script>
const dialog = document.querySelector('#newsletter')
document.querySelector('#open').addEventListener('click', () => {
dialog.showModal()
})
</script>
showModal() открывает окно на верхнем слое (top layer) (поверх другого контента), добавляет фон, который можно стилизовать с помощью dialog::backdrop, захватывает фокус и делает остальную страницу инертной. Esc закрывает его. Форма с method="dialog" закрывает диалог при отправке, поэтому кнопке закрытия не нужен JavaScript.
В чем разница между show() и showModal()? Только showModal() предоставляет фон и захват фокуса. Клик за пределами окна не закрывает его по умолчанию. Атрибут closedby="any" добавляет этот функционал, но пока только в Chrome 134 и Firefox 141. Для поддержки такого функционала во всех браузерах требуется небольшой обработчик.
Поддержка: все основные браузеры с сентября 2024 года.
Прим. пер.: Invoker Commands API позволяет создавать модалки и поповеры (см. ниже) полностью декларативно.
❯ 3. Popover API
Всплывающие подсказки (tooltips) и выпадающие меню (dropdown menus) были причиной, по которой многие из нас устанавливали tippy.js, которая работала поверх popper.js, которая позже стала Floating UI. Библиотеки выполняли 2 задачи: показывали и скрывали элемент с правильными правилами отмены (dismissal rules) и располагали его рядом с триггером.
Popover API выполняет первую задачу. 2 атрибута и никакого JS:
<button popovertarget="share-menu">Share</button>
<div id="share-menu" popover>
<a href="https://x.com/intent/post?url=https://flaviocopes.com">Post on X</a>
<a href="https://news.ycombinator.com/submitlink?u=https://flaviocopes.com">Submit to HN</a>
</div>
Клик по кнопке открывает меню. Клик за пределами меню или нажатие Esc закрывает его. Это поведение легкой отмены (light dismiss), которое каждая библиотека реализовывала вручную с помощью обработчика кликов на уровне документа. Поповер также рендерится на верхнем уровне, поэтому никогда не проигрывает битву за z-index с липкой шапкой (sticky header). В JS нам доступны методы showPopover(), hidePopover() и togglePopover(), а также событие toggle, которое возникает при каждом изменении.
Что насчет второй задачи? Popover API ничего не позиционирует. По умолчанию поповер появляется в середине области просмотра (браузер стилизует его с помощью inset: 0 и margin: auto), а не рядом с кнопкой. Размещение его рядом с триггером - задача якорного позиционирования (anchor positioning) CSS, которое поддерживается всеми основными браузерами с сентября 2026 года (прим. пер.: у якорного позиционирования пока есть проблемы, мешающие его использования в продакшне), поэтому Floating UI по-прежнему нужен для позиционирования. Значение popover="hint" для тултипов, которые не закрывают другие поповеры, новое и пока поддерживается только в Chrome 151 и Firefox 153.
Поддержка: все основные браузеры с января 2025 года.
❯ 4. API переходов отображения
Анимация между страницами на сайте, который рендерится на сервере, означала использование barba.js или swup. Они перехватывают каждый клик по ссылке, получают данные следующей страницы с помощью fetch(), меняют контент страницы и анимируют переход. Мы получаем плавные переходы и роутер, о котором не просили, с собственными правилами для скриптов, положения прокрутки и аналитики.
View Transitions API выполняет анимацию в браузере. Для изменений внутри одной страницы достаточно обернуть обновление DOM в вызов метода:
document.startViewTransition(() => {
list.prepend(newItem)
})
Браузер делает снимок (snapshot) перед обновлением, выполняет коллбек, делает еще один снимок и меняет снимки местами. Анимацию можно кастомизировать с помощью ::view-transition-old(root) и ::view-transition-new(root), а элементы с view-transition-name получают собственную анимацию обновления.
Для реальной межстраничной навигации, нужно включить одно правило CSS на обеих страницах:
@view-transition {
navigation: auto;
}
Здесь нет ни роутера, ни перехваченного клика. Браузер выполняет нормальную навигацию и анимирует переход между старым и новым документами.
Что насчет поддержки? Переходы в одном документе (одностраничные приложения - SPA) (startViewTransition()) поддерживаются всеми основными браузерами с октября 2025 года. Переходы между документами (@view-transition) пока поддерживаются только в Chrome 126 и Safari 18.2. Пользователи Firefox получают обычную навигацию, что является нормальным резервом, но если анимация переходов является критичной для вашего дизайна, тогда вам по-прежнему нужна библиотека.
❯ 5. AbortController и получение данных с ним
fetch() сам по себе заменяет $.ajax() и большую часть того, для чего устанавливался axios. Я напрямую использую fetch() в большинстве своих проектов. В первые годы он не предоставлял возможности отменять запросы, что приводило к установке axios в большом количестве кодовых баз.
AbortController закрывает эту брешь:
const controller = new AbortController()
fetch('https://api.github.com/repos/flaviocopes/tte-js', {
signal: controller.signal,
})
controller.abort()
Промис отклоняется с ошибкой, чье свойство name имеет значение AbortError. Во многих случаях контроллер не нужен, например:
const response = await fetch('https://flaviocopes.com/rss.xml', {
signal: AbortSignal.timeout(5000),
})
Я использую это в Port Pilot - инструменте, который проверяет локальные порты на моем Mac. Некоторые порты принимают подключение, но никогда не отвечают, поэтому каждая проверка длится 450 мс, после чего инструмент переходит к следующему порту. Без отмены (abort), один застрявший сервис замедляет все сканирование.
Сигнал отмены также можно использовать с addEventListener(), один abort() заменяет каждый зарегистрированный с его помощью обработчик, а AbortController.any() комбинирует пользовательскую отмену и таймаут в один сигнал. Токены отмены (cancel tokens) axios признаны устаревшими, вместо них рекомендуется использовать AbortSignal .
Подсказка: таймаут отклоняется с TimeoutError, а не с AbortError, поэтому проверяйте error.name, когда эти ошибки должны обрабатываться по-разному. Отмена прекращает ожидание выполнения запроса в коде. Если запрос достиг сервера, сервер будет его обрабатывать, несмотря на отмену.
Поддержка: AbortController поддерживается всеми основными браузерами с 2021 года, а AbortSignal.timeout() - с апреля 2024 года.
❯ 6. structuredClone()
Глубокое копирование объектов было главной причиной установки lodash ради cloneDeep или rfdc ради более высокой производительности. Многие ничего не устанавливали и писали JSON.parse(JSON.stringify(obj)), надеясь, что объект не содержит дат.
structuredClone() решает эту проблему:
const order = {
id: 1,
items: [{ sku: 'javascript-handbook', qty: 1 }],
created: new Date(),
}
const copy = structuredClone(order)
copy.items[0].qty = 2
order.items[0].qty //1
copy.created instanceof Date //true
Он использует тот же алгоритм, что браузер для передачи данных веб-воркеру (Web Worker) или их сохранения в IndexedDB. Вложенные массивы и объекты копируются и, в отличие от JSON, сохраняются Date, Map, Set, RegExp, типизированные (typed) массивы и undefined. Также правильно обрабатываются циклические зависимости.
Генератор тем Tailwind клонирует объект дефолтного состояния при каждом сбросе или выборе пресета, поэтому редактирование текущей темы никогда не затрагивает дефолтные значения.
Нюанс: если объект содержит функцию или символ, выбрасывается DataCloneError. Экземпляры классов возвращаются в виде обычных объектов. Методы пропадают, а copy instanceof Cart возвращает false. Если данные содержат поведение, по-прежнему нужен собственный метод копирования. lodash.cloneDeep() копирует функции по ссылке и сохраняет прототипы.
Поддержка: все основные браузеры с сентября 2024 года.
❯ 7. Object.groupBy()
Группировка массива по ключу была второй главной причиной установки lodash. _.groupBy(posts, 'tag') - короткая строка против ручной реализации reduce() - каждый раз оправдывало зависимость.
Object.groupBy() решает эту проблему:
const posts = [
{ title: 'The Fetch API', tag: 'js' },
{ title: 'CSS Grid', tag: 'css' },
{ title: 'Arrow functions', tag: 'js' },
]
const byTag = Object.groupBy(posts, (post) => post.tag)
byTag.js.length // 2
Object.keys(byTag) // ['js', 'css']
Коллбек возвращает ключ группы для каждого элемента, и мы получаем одно свойство на группу. Map.groupBy() делает тоже самое и возвращает Map, когда ключом является объект и нужно сохранить его тип.
Возвращаемое значение - объект с нулевым прототипом, поэтому byTag.hasOwnProperty('js') выбрасывает TypeError, поскольку hasOwnProperty() отсутствует. Используйте Object.hasOwn(byTag, 'js') или 'js' in byTag. Ключи также приводятся к строкам, как любые ключи объекта, поэтому группировка по числовому году даст '2026', например, а отсутствующая группа будет иметь значение undefined, поэтому byTag.php.length выбросит исключение.
Поддержка: все основные браузеры с сентября 2026 года.
❯ 8. toSorted(), toReversed(), toSpliced() и with()
sort() и reverse() модифицируют исходный массив, и от этого хотя бы раз страдал каждый, обычно, в компоненте React, где отсортированный список имел состояние. Обходными путями были [...items].sort() или sortBy() из lodash, возвращающие новый массив, или вспомогательная библиотека, вроде immutability-helper.
Четыре новых метода возвращают новый массив и не трогают исходный:
const prices = [29, 9, 49]
const sorted = prices.toSorted((a, b) => a - b)
prices // [29, 9, 49]
sorted // [9, 29, 49]
toReversed() - это копирующая версия reverse() и т.д., а with(index, value) заменяет один элемент и возвращает новый массив:
prices.with(1, 10) // [29, 10, 49]
Есть парочка нюансов. toSorted() без компаратора сортирует элементы как строки, поэтому [29, 9, 49].toSorted() дает [29, 49, 9]. Всегда передавайте компаратор для чисел. with() более строгий, чем присвоение: prices.with(5, 1) выбрасывает RangeError, а prices[5] = 1 расширяет массив пустыми слотами. Методов toPushed() и toShifted() нет, для таких случаев по-прежнему нужно создавать новый массив вручную.
Поддержка: все основные браузеры с января 2026 года.
❯ 9. Web Crypto
Хеширование строки или подписание (signing) токена в браузере означало использование crypto-js. README этой библиотеки рекомендует использовать нативный модуль Crypto, последний ее релиз состоялся в октябре 2023 года. На сервере jsonwebtoken оборачивает crypto Node.js для аналогичного шага HMAC.
crypto.subtle работает во всех современных браузерах и Node.js. Подписание HS256 JWT, за что отвечала jsonwebtoken, легко делается с помощью importKey() и sign():
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
encoder.encode('a-long-random-secret-from-your-env'),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(`${header}.${payload}`)
)
signature - это ArrayBuffer, размером 32 байта. Кодируем его в base64url, добавляем в header.payload и получаем токен. Весь поток (flow), включая то, почему верификация не должна полагаться на alg в заголовке, описан в статье “Как на самом деле работает подписание JWT”, а JWT signer tool позволяет выполнять этот код в вашем браузере.
Хеширование еще проще:
const digest = await crypto.subtle.digest('SHA-256', encoder.encode('hello'))
const hex = [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
Поскольку тот же API существует в Cloudflare Workers, веб-хуке, обрабатывающем покупки курсов на этом сайте, RSA-подпись Paddle проверяется с помощью crypto.subtle.verify(), а конечные точки формы хешируют IP-адреса посетителей с помощью digest(), прежде чем использовать их в качестве ключей ограничения количества запросов (rate limit).
Подсказка: crypto.subtle доступен только в безопасном контексте, таком как HTTPS или localhost. На обычной странице http:// он имеет значение undefined. Все асинхронное и возвращает ArrayBuffer, поэтому утилиты для hex и base64url кодирования пишутся вручную. MD5 отсутствует, его запрос выбрасывает NotSupportedError.
Поддержка: все основные браузеры с 2020 года.
❯ 10. Temporal
Даты - причина, по которой везде устанавливалась Moment.js и многие проекты содержат moment, dayjs или date-fns. Date знает лишь временную зону системы и UTC, мутирует на месте, считает месяцы с 0 и разбирает (parse) не ISO-строки по-разному в разных движках.
Temporal - замена, встроенная в язык. Каждое значение иммутабельно, календарные даты и точные мгновения (instants) имеют разные типы, а временные зоны являются объектами первого класса:
const launch = Temporal.PlainDate.from('2026-09-19')
launch.add({ weeks: 1 }).toString() //'2026-09-26'
const rome = Temporal.Now.zonedDateTimeISO('Europe/Rome')
rome.timeZoneId //'Europe/Rome'
Различия представлены в виде Duration, который можно запросить для нужной единицы:
const start = Temporal.PlainDate.from('2026-08-10')
const end = Temporal.PlainDate.from('2026-09-17')
end.since(start).days // 38
start.until(end, { largestUnit: 'months' }).toString() //'P1M7D'
Другая важная фича Moment, относительное время (например, “3 дня назад”), уже была заменена Intl.RelativeTimeFormat, который поддерживается всеми основными браузерами с 2020 года. Я рассказывал о нем в статье “Интернационализация в JavaScript”.
Поддержка - то, что отличает Temporal от всего вышеперечисленного. По состоянию на сентябрь 2026 года он поставляется в Firefox начиная с версии 139 (май 2025 года) и в Chrome начиная с версии 144 (январь 2026 года), в то время как в Safari он доступен только в режиме Safari Technology Preview, стабильного релиза пока нет. Это означает, что в продакшне нужен @js-temporal/polyfill. В Node.js 26 Temporal включен по умолчанию, а в Deno он есть начиная с версии 2.7, поэтому скрипту, который работает только с Temporal, не нужен полифил.
В документации Moment он назван устаревшим проектом, находящимся в режиме поддержки. day.js и date-fns по-прежнему поддерживаются и работают нормально. Temporal внедряет ту же модель в сам язык, а полифил утратит актуальность после добавления поддержки в Safari.
Еще один нюанс: Temporal.PlainDate.from() принимает объект со свойствами или строку RFC 9557 - это формат ISO 8601 с опциональными аннотациями. В Moment намеренно убрали снисходительный парсер, который принимал практически что угодно. Сначала проверяйте ввод пользователя, а затем создавайте значение Temporal.
❯ Заключение
Перед использованием npm проверьте Baseline. MDN отображает статус в верхней части каждой страницы API, а NPM-пакет web-features содержит те же данные в формате JSON, если вы хотите использовать их в скрипте.
“Широко доступно” (widely available) означает, что каждый движок поддерживает фичу как минимум 30 месяцев, и можно отказаться от библиотеки. “Доступно недавно” (newly available) означает, что все три основных движка поддерживают фичу, но старые версии еще актуальны, поэтому принимайте решение самостоятельно. При ограниченной доступности (limited availability), как, например, в случае Temporal, библиотека или полифил остаются, и вы пишете код, используя стандартный API, так что отказ от библиотеки или полифила позже будет означать всего лишь удаление импорта.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.