JavaScript в браузере: DOM, события, асинхронность
Десять сниппетов для клиентской части: debounce и throttle, определение редактируемой цели события, плавная прокрутка к якорю, ленивая загрузка автовоспроизводимого видео, объединение Tailwind-классов, выбор контраста и чтение payload JWT.
Браузерный код ломается не так, как серверный: пользователь скроллит во время анимации, печатает быстрее обработчика, открывает страницу с уже готовым якорем в адресе или живёт с заблокированным автовоспроизведением. Каждый из этих десяти сниппетов закрывает одну такую ситуацию.
Debounce и throttle — не синонимы
Оба ограничивают частоту вызова, и их регулярно путают. Debounce ждёт паузы — это то, что нужно поисковой строке, потому что важно финальное значение, а не промежуточные. Throttle выполняет с фиксированной максимальной частотой — это то, что нужно обработчику скролла или ресайза, потому что требуется периодическое обновление, и получить устаревшее лучше, чем не получить никакого. Debounce на скролле даёт интерфейс, который обновляется только после остановки, и это обычно читается как поломка.
Горячие клавиши и проблема редактируемой цели
Любое приложение с односимвольными шорткатами рано или поздно срабатывает, пока пользователь печатает в форме. Проверка event.target.tagName === 'INPUT' — типичная первая попытка, и она пропускает textarea, области с contenteditable и элементы внутри shadow root. Проверять нужно все три случая, и именно это отличает продуманную систему шорткатов от той, что съедает символы.
Автовоспроизведение — это разрешение, а не настройка
Браузеры блокируют автозапуск видео со звуком, а беззвучный автозапуск всё равно тратит трафик и декодирование на каждое видео в документе. Привязка воспроизведения к IntersectionObserver означает, что видео стартует, только когда реально попало на экран, и останавливается при уходе, — это одновременно и вежливое, и быстрое поведение. Справочник MDN по автовоспроизведению описывает, какие комбинации разрешают актуальные браузеры.
Прочитать JWT — не значит проверить его
Сниппет с payload декодирует base64url и возвращает claims. Больше он ничего не делает, и различие принципиально: кто угодно может собрать токен, в payload которого написано что угодно. Декодирование на клиенте годится, чтобы решить, что показать; оно никогда не годится, чтобы решить, что разрешить. Проверка требует ключа подписи и живёт на сервере.
Хелперы цвета и объединения классов
Решение, какой текст класть на произвольный фон — белый или чёрный, — это расчёт воспринимаемой яркости, а не светлоты: наивный вариант (r+g+b)/3 грубо ошибается на зелёном. А объединение Tailwind-классов — не конкатенация строк: px-2 px-4 оставляет в списке оба и отдаёт решение порядку специфичности, поэтому и существует отдельный хелпер слияния.
Безопасное объединение Tailwind-классов (хелпер cn)
Комбинирует условные классы через clsx и разрешает конфликты Tailwind-утилит через tailwind-merge — стандартный хелпер "cn()", используемый в кодовых базах в стиле shadcn/ui.
Как это работает: clsx схлопывает массив строк/объектов/falsy-значений в одну строку классов; затем twMerge разрешает конфликты конкретно Tailwind-утилит (например, px-2 против px-4), оставляя только тот, что должен победить, вместо того чтобы оставлять оба класса в DOM. Требует npm install clsx tailwind-merge.
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs) {
return twMerge(clsx(inputs))
}
// Example usage — later classes win over earlier conflicting ones
cn('px-2 py-1', condition && 'px-4', 'text-sm')
// condition = true -> "py-1 px-4 text-sm"
// condition = false -> "px-2 py-1 text-sm"
Дебаунс функции
Откладывает выполнение функции до тех пор, пока серия вызовов не прекратится — идеально для полей поиска и обработчиков resize.
Как это работает:
- При каждом вызове debounced-функции предыдущий отложенный таймер сбрасывается.
- Планируется новый таймер на
delayмс. - Обёрнутая функция реально выполняется только тогда, когда вызовы прекращаются на
delayмс.
function debounce(fn, delay = 300) {
let timeoutId
return function debounced(...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Example usage
const onResize = debounce(() => console.log('window resized'), 250)
window.addEventListener('resize', onResize)
Троттлинг функции
Гарантирует, что функция выполняется не чаще, чем раз в X миллисекунд — идеально для обработчиков scroll и mousemove, которые иначе срабатывали бы сотни раз в секунду.
Как это работает: в отличие от debounce (который ждёт, пока вызовы прекратятся), throttle гарантирует, что функция продолжает выполняться периодически ВО ВРЕМЯ непрерывного потока вызовов — первый вызов срабатывает сразу, а последующие игнорируются, пока не пройдёт limit мс.
function throttle(fn, limit = 200) {
let waiting = false
return function throttled(...args) {
if (waiting) return
fn.apply(this, args)
waiting = true
setTimeout(() => { waiting = false }, limit)
}
}
// Example usage
const onScroll = throttle(() => console.log('scroll position:', window.scrollY), 200)
window.addEventListener('scroll', onScroll)
Проверка, светлый цвет или тёмный (hex)
Вычисляет воспринимаемую яркость hex-цвета, чтобы определить, какой текст лучше читается поверх него — чёрный или белый.
Как это работает: hex-цвет конвертируется в RGB, после чего применяется стандартная формула яркости с учётом восприятия глаза (299r + 587g + 114b) — значения выше 155 считаются "светлым" фоном.
function isColorLight(hex) {
hex = hex.replace(/^#/, '')
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
const r = parseInt(hex.substring(0, 2), 16)
const g = parseInt(hex.substring(2, 4), 16)
const b = parseInt(hex.substring(4, 6), 16)
const brightness = (r * 299 + g * 587 + b * 114) / 1000
return brightness > 155
}
// Example usage — pick readable text color for any background
const textColor = isColorLight('#F5D90A') ? 'black' : 'white'
Проверка, является ли цель события редактируемым полем
Защищает глобальные горячие клавиши от срабатывания, пока пользователь печатает в input, textarea, select или contenteditable-элементе.
Как это работает: проверяется флаг isContentEditable элемента и его тег на совпадение с обычными полями ввода текста, чтобы можно было пропустить обработку горячей клавиши, пока пользователь активно печатает.
function isEditable(target) {
if (!target) return false
const tag = (target.tagName || '').toLowerCase()
return target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select'
}
// Example usage — only trigger a shortcut when not typing
window.addEventListener('keydown', (e) => {
if (e.key === '/' && !isEditable(document.activeElement)) {
e.preventDefault()
// focus a search input, open a command palette, etc.
}
})
Lazy-load автовоспроизводимых видео через IntersectionObserver
Откладывает загрузку видео до появления в viewport, а затем один раз запускает воспроизведение без звука — контент ниже первого экрана не попадает в первоначальную загрузку.
Как это работает: preload="none" просит браузер не загружать видео заранее, а отсутствие autoplay позволяет передать первый запуск под контроль observer. Атрибуты muted и playsinline важны для политик автовоспроизведения и inline-воспроизведения на мобильных устройствах.
<video
class="lazy"
width="720"
height="404"
loop
muted
playsinline
preload="none"
poster="/images/ocean.png"
src="/videos/demo/ocean.mp4"
></video>
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return
const video = entry.target
video.play().catch(() => {
// Браузер всё ещё может заблокировать автовоспроизведение.
})
observer.unobserve(video)
})
})
document.querySelectorAll('.lazy').forEach((video) => {
observer.observe(video)
})
Плавный скролл к якорю
Прокручивает страницу к элементу по ID с учётом смещения (например, высоты fixed-хедера) — без jQuery, нативный smooth scroll.
Как это работает: getBoundingClientRect().top возвращает позицию элемента относительно viewport. Прибавляем window.scrollY, чтобы получить абсолютную позицию, и вычитаем offset для хедера. scrollTo({ behavior: 'smooth' }) делает анимацию нативно.
function scrollToAnchor(id, offsetPx = 0) {
const el = document.getElementById(id)
if (!el) return
const top = el.getBoundingClientRect().top + window.scrollY - offsetPx
window.scrollTo({ top, behavior: 'smooth' })
}
// Версия с поддержкой prefers-reduced-motion
function scrollToAnchorAccessible(id, offsetPx = 0) {
const el = document.getElementById(id)
if (!el) return
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const top = el.getBoundingClientRect().top + window.scrollY - offsetPx
window.scrollTo({ top, behavior: prefersReduced ? 'instant' : 'smooth' })
}
// Example usage
// Навигационная ссылка с хедером 64px
document.querySelectorAll('a[href^="#"]').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault()
const id = link.getAttribute('href').slice(1)
scrollToAnchorAccessible(id, 64)
})
})
sleep() / задержка в async/await
Минималистичная функция sleep для JavaScript и TypeScript — приостанавливает выполнение async-функции на заданное количество миллисекунд без сторонних библиотек.
Как это работает: возвращает Promise, который резолвится через setTimeout. await на нём приостанавливает только текущую async-функцию, не блокируя event loop.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// Example usage — задержка между попытками
async function pollUntilReady(checkFn, intervalMs = 500, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
if (await checkFn()) return true
await sleep(intervalMs)
}
return false
}
// Задержка между пакетными запросами (rate limiting на стороне клиента)
for (const batch of batches) {
await processBatch(batch)
await sleep(200)
}
Получение геолокации по IP (ipapi.co)
Определяет страну и город посетителя по его IP-адресу через бесплатный API ipapi.co.
Как это работает: GET-запрос к ipapi.co/{ip}/json/ возвращает данные геолокации в JSON — включая country_name и city. Если оставить ip пустым, API само определит IP вызывающей стороны, поэтому это отлично подходит для приветствия текущего посетителя.
async function getIpInfo(ip = '') {
const response = await fetch(`https://ipapi.co/${ip}/json/`)
return response.json()
}
// Example usage
const info = await getIpInfo() // empty ip -> detects the visitor's own IP
console.log(`${info.city}, ${info.country_name}`)
const other = await getIpInfo('8.8.8.8') // or look up a specific IP
console.log(other.country_name)
Декодировать payload JWT без верификации
Извлекает payload из JWT-токена на клиенте без проверки подписи — удобно для чтения exp, userId и ролей в UI. Никогда не использовать для авторизации на сервере.
Как это работает: JWT состоит из трёх частей, разделённых точками. Вторая часть — Base64URL-encoded JSON с полезной нагрузкой. Функция декодирует её и парсит JSON. Подпись не проверяется — это только для чтения данных на клиенте.
function decodeJwtPayload(token) {
try {
const base64 = token.split('.')[1]
if (!base64) return null
// Base64URL → Base64
const normalized = base64.replace(/-/g, '+').replace(/_/g, '/')
return JSON.parse(atob(normalized))
} catch {
return null
}
}
function isTokenExpired(token) {
const payload = decodeJwtPayload(token)
if (!payload?.exp) return true
return Date.now() >= payload.exp * 1000
}
// Example usage
const payload = decodeJwtPayload(localStorage.getItem('token'))
// { sub: '42', role: 'admin', exp: 1720000000 }
if (isTokenExpired(token)) {
// обновить токен или разлогинить
}
Другие подборки сниппетов
Контактная информация
Оставьте заявку или напишите напрямую в телеграм. Я отвечу в течение 24 часов.