Skip to content
← К сниппетам
JavaScript

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 оставляет в списке оба и отдаёт решение порядку специфичности, поэтому и существует отдельный хелпер слияния.

JavaScriptJavaScript

Безопасное объединение 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"
JavaScriptJavaScript

Дебаунс функции

Откладывает выполнение функции до тех пор, пока серия вызовов не прекратится — идеально для полей поиска и обработчиков resize.

Как это работает:

  1. При каждом вызове debounced-функции предыдущий отложенный таймер сбрасывается.
  2. Планируется новый таймер на delay мс.
  3. Обёрнутая функция реально выполняется только тогда, когда вызовы прекращаются на 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)
JavaScriptJavaScript

Троттлинг функции

Гарантирует, что функция выполняется не чаще, чем раз в 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)
JavaScriptJavaScript

Проверка, светлый цвет или тёмный (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'
JavaScriptJavaScript

Проверка, является ли цель события редактируемым полем

Защищает глобальные горячие клавиши от срабатывания, пока пользователь печатает в 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.
  }
})
JavaScriptJavaScript

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)
})
JavaScriptJavaScript

Плавный скролл к якорю

Прокручивает страницу к элементу по 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)
  })
})
JavaScriptJavaScript

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)
}
JavaScriptJavaScript

Получение геолокации по 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)
JavaScriptJavaScript

Декодировать 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)) {
  // обновить токен или разлогинить
}
// CONNECT

Контактная информация

Оставьте заявку или напишите напрямую в телеграм. Я отвечу в течение 24 часов.

Укажите email или Telegram, чтобы я мог вам ответить.