Frontend VK Hub
СтатистикаКомьюнити VK для фронтендеров. Кто и что стоит за интерфейсами, которыми пользуются миллионы пользователей, — от Почты Mail до VK Teams — рассказываем здесь 🤘
- Последний пост
- 13 авг.
- Последнее чтение
- 09:12
- Постов за неделю
- 2
- Всего постов
- 37
- Тип
- открытый
- Язык
- русский
- Категория
- Технологии
- В каталоге с
- 12 авг.
- 1/24сутки в ленте
- 214
- 1/48двое суток
- 245
- 1/72трое суток
- 264
Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.
Посты
CSS @scope — изоляция стилей без Shadow DOM и препроцессоров Изоляция стилей в CSS долго решалась косвенно: BEM с длинными именами, CSS Modules с хешированием, scoped-атрибут в Vue, Shadow DOM в Web Components. У каждого варианта своя цена. @scope даёт нативный способ ограничить область действия правил. В Chrome 118 с октября 2023, Safari 17.4 с марта 2024, Firefox 143 с сентября 2025 — и в 2026 везде. Идея простая. Внутри @scope правила применяются только к элементам в поддереве заданного корня: @scope (.card) { h2 { color: purple; } p { line-height: 1.6; } } Все h2 и p внутри .card получат эти стили. Классов вроде .card__title не нужно: селектор внутри scope пишется как есть, границу задаёт сам scope. Верхняя граница задаётся через to: @scope (.article) to (.comments) { p { font-size: 1.1rem; } } Правило применяется ко всем p внутри .article, но не проникнет в .comments и вложенное. Решает классическую задачу: статья и блоки комментариев стилизуются независимо, но рендерятся внутри одной иерархии. Второй вариант — inline-scope через <style> внутри HTML. Тег <style> с @scope без селектора привязывается к родителю: <div class="widget"> <style> @scope { h3 { color: teal; } button { background: navy; } } </style> <h3>Заголовок</h3> <button>Кнопка</button> </div> Ближайший CSS-аналог <style scoped> из Vue: описываем стили прямо в компоненте, они применяются только к его поддереву. Без прекомпилятора и обёрток. Правила внутри @scope считаются с той же специфичностью, что и снаружи, но при равной выигрывает правило, находящееся ближе к элементу по DOM. Правило из внутреннего @scope перебьёт правило из внешнего. Практический паттерн — темизация секции: @scope (.section--dark) { :scope { background: #1a1a1a; color: #eee; } a { color: #7cb; } button { background: #333; color: #eee; } } :scope — сам элемент, к которому привязана область (аналог :host в Shadow DOM). Всё внутри .section--dark перекрашивается разом, без каскадных префиксов и без глобальных override. @scope ограничивает только те правила, что внутри самого @scope. Не блокирует утечку правил снаружи: если во внешнем CSS .card p { color: red }, оно применится к p внутри scope. Если в проекте BEM используется только ради изоляции, или CSS Modules подключены только для избежания конфликтов имён, @scope часто закрывает задачу без сборочного шага. #frontendvkhub #css #scope
Promise.withResolvers() — deferred без танцев с замыканиями Есть паттерн, который в JS годами делали руками: получить промис плюс функции resolve и reject, чтобы дёрнуть их снаружи. Каждый раз конструкция с замыканием. С 2024 есть Promise.withResolvers(): три сущности одной строкой. Baseline: Chrome 119, Safari 17.4, Firefox 121, Node 22+. Как это выглядит Раньше было так: let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); // а дальше где угодно button.addEventListener('click', () => resolve('clicked')); Теперь короче: const { promise, resolve, reject } = Promise.withResolvers(); button.addEventListener('click', () => resolve('clicked')); Кажется мелочью, но паттерн возникает часто. Рассмотрим три кейса. Конвертация event-based API в await Ждём одно событие и продолжаем: async function waitForClick(button) { const { promise, resolve } = Promise.withResolvers(); button.addEventListener('click', resolve, { once: true }); return promise; } const evt = await waitForClick(saveButton); Раньше — new Promise с замыканием, теперь одна строка настройки и return promise. Промис, которым управляют извне модуля Экспортируем промис, резолвящийся при готовности: инициализация SDK, авторизация, connection ready: const { promise: ready, resolve: markReady } = Promise.withResolvers(); export function initSDK(config) { setup(config).then(() => markReady()); } export { ready }; Другие модули просто await ready перед использованием SDK. Без withResolvers пришлось бы либо экспортировать промис отдельно, либо городить lazy-инициализацию. Обёртка над колбэками Много старых API отдают результат через колбэк, обёртка с withResolvers чище: function toPromise(fn) { const { promise, resolve, reject } = Promise.withResolvers(); fn((err, result) => { if (err) reject(err); else resolve(result); }); return promise; } const data = await toPromise(cb => oldApi.load(cb)); То же, что делает util.promisify в Node, только локально и без зависимостей. Один нюанс с TypeScript. Раньше тип для deferred писали руками. С withResolvers TypeScript выводит тип автоматически, дженерик указывается при вызове: const { promise, resolve } = Promise.withResolvers<string>(); resolve('hello'); // ok resolve(42); // error Promise.withResolvers() создаёт промис, у которого resolve и reject доступны снаружи. Удобно, но может быть опасно: любой, кто получил ссылку на resolve, может завершить промис из произвольного места. Не пробрасывайте эти функции наружу модуля без нужды — держим их приватными, отдаём только сам промис. Если в кодовой базе есть паттерн let resolve; new Promise(r => resolve = r) — время заменить на Promise.withResolvers. Одна строка, читается очевидно, TypeScript выводит типы сам.
без подписи
без подписи
без подписи
без подписи
без подписи
Temporal API Почти в каждом проекте найдётся место, где дату надо сдвинуть на месяц вперёд или посчитать разницу между двумя днями. И почти всегда ради этого в зависимостях лежит date-fns или dayjs, потому что штатный Date для такой работы не приспособлен: он мутабельный, нумерует месяцы с нуля и знает ровно одну зону, системную. Temporal переносит эту арифметику в сам язык, и за последний год он перестал быть предметом ожидания. В Firefox поддержка появилась ещё в 139, в январе 2026 её добрали Chrome и Edge в версии 144, а в марте предложение дошло до Stage 4 и вошло в ES2026. Поэтому разговор сместился к переводу существующего кода, и об этом расскажем в карточках: какие в Temporal типы, как он считает даты и зоны и как всё это стыкуется со старым Date. #frontendvkhub #temporal
🔵Chrome 150: новые возможности CSS Chrome в июле раскатал 150 в стейбл на основную массу пользователей — и это, пожалуй, главное событие месяца, потому что там сразу пачка возможностей CSS, ради которых давно писали костыли. text-fit автоматически подгоняет размер шрифта под ширину контейнера — до сих пор это делалось через ResizeObserver с пересчётом font-size на каждый resize, теперь одна строка CSS. background-clip: border-area наконец сделал возможными градиентные и image-fill бордеры без псевдоэлементов и хитрых box-shadow — фичу ждали примерно с эпохи CSS Gradients. focusgroup — декларативный атрибут для клавиатурной навигации стрелками. В каждом кастомном селекте или меню раньше приходилось руками писать keydown-обработчики и следить за фокусом. 🔵Chrome 151: доступ к анимациям упростили Через несколько дней после 150 подоспел 151 в бете. animation теперь доступен прямо на AnimationEvent и TransitionEvent: раньше в обработчике звали getAnimations() и матчили нужное по имени, теперь объект достаётся из ивента. Плюс ruby-overhang для CJK. Из breaking — 151 больше не запустится на macOS 12, минимум теперь 13, так что если пользователи на старых Mac ещё есть, стоит глянуть свою статистику. 🔵Firefox: AI-ассистент в широком доступе и нативная поддержка Containers Firefox параллельно занят пользовательским опытом. 152.0.6 от 14 июля докатил Smart Window до широкого rollout: выделили текст на странице, всплывает встроенный AI-ассистент с опциями summarize, explain, rewrite. То, что раньше делали сторонние расширения, теперь встроено. А 21 июля вышел 153 ESR с превью нативных Containers — вместо Multi-Account Containers extension теперь родная функциональность. Плюс частичная поддержка ::-webkit-scrollbar в CSS для web compat со старыми сайтами. 🔵Vue: патчи для hydration, reactivity и SSR Vue шёл по мелким патчам — hydration edge cases, reactivity в scope stop, preserving textarea resize, deferred mount для teleport. Ничего headline-фичного, но если проект на Nuxt и SSR-heavy, свежую минорку стоит поймать. #frontendvkhub #дайджест
CSS Anchor Positioning — tooltip и меню без Popper.js Позиционирование tooltip относительно кнопки годами было болью. Popper.js и Floating UI берут на себя всё: координаты, scroll, resize, переполнение viewport, разворот тултипа при нехватке места. С 2024 CSS сам умеет привязывать элементы. Chrome 125, Safari 26, Firefox 147 — везде Baseline. Идея очень простая. Один элемент помечается как anchor-name: --my-button, другой ссылается через position-anchor: --my-button и вычисляет позицию через anchor(): <button id="menu-btn" style="anchor-name: --menu-btn"> Меню </button> <div popover id="menu"> Пункт 1 Пункт 2 </div> #menu { position-anchor: --menu-btn; top: anchor(bottom); left: anchor(left); } anchor(bottom) возвращает координату нижней границы якоря, anchor(left) — левой. Меню оказывается ровно под кнопкой у её левого края. Никакого JS, никакого пересчёта на scroll — браузер держит связь сам. Второй важный кусок — position-try. Что, если снизу не хватает места, и меню обрежется viewport? Попробовать другую позицию: @position-try --menu-flip-up { top: auto; bottom: anchor(top); } #menu { position-anchor: --menu-btn; top: anchor(bottom); left: anchor(left); position-try-fallbacks: --menu-flip-up; } Браузер сначала пробует основную позицию, при выходе за viewport применяет --menu-flip-up и разворачивает меню наверх, привязываясь к верху кнопки. Именно это Popper.js делал через flip middleware. Теперь в платформе. Для типичных сценариев есть шорткат position-area. Вместо явных top/left задаётся направление: #tooltip { position-anchor: --btn; position-area: top; /* сверху над якорем */ /* или: top left, bottom right, center, start end */ } position-area: top автоматически ставит bottom: anchor(top) и правильное горизонтальное выравнивание. Закрывает большинство кейсов для тултипов. Отдельно — приятная работа с Popover API. Тултипы и меню ставятся в top layer через popover, не воюют с overflow: hidden и z-index родителей. Anchor Positioning работает с поповерами прозрачно: <button popovertarget="hint" style="anchor-name: --hint-btn">?</button> <div id="hint" popover style="position-anchor: --hint-btn; position-area: top"> Подсказка </div> Клик на ? открывает поповер, он сам позиционируется над кнопкой, Escape и клик снаружи закрывают. Без JS. Anchor работает только с position: absolute или fixed (для popover автоматически). Если anchor-name у нескольких элементов, работает первый в DOM-порядке. И anchor() нельзя прописать в animation и transition — только в свойствах позиции. Если Popper.js или Floating UI в проекте ради простых тултипов и dropdown — самое время посмотреть, что закрывается нативно. Библиотеки остаются нужны для сложной физики (виртуальные якоря, драг), но 80% типичных случаев уходят в CSS. А вы уже пробовали Anchor Positioning? #frontendvkhub #css
без подписи
без подписи
без подписи
без подписи
без подписи
без подписи
без подписи
useEffect: семь случаев, когда он не нужен React 19 сужает область useEffect до одной задачи: синхронизация с внешними системами. Деривация UI из пропсов и стейта происходит в рендере, обработка действий — в обработчиках событий, загрузка данных уходит во фреймворк. В карточках рассмотрим семь случаев, когда useEffect не нужен. #frontendvkhub #react #useeffect
TypeScript 7.0 — это не только новый синтаксис. Главное изменение в том, что команда Microsoft перенесла компилятор с TypeScript и JavaScript на Go. Какие новые настройки действительно стоит учитывать при переходе, рассказывает старший фронтенд-разработчик VK Марат Исаев. Новый компилятор на Go До TypeScript 7.0 компилятор был на TypeScript и выполнялся как JavaScript-код в Node.js. Для языка это было удобно, ведь TypeScript развивался на самом себе. Но в больших проектах такая архитектура влияла на производительность: проверка типов — тяжёлая вычислительная задача, а старый компилятор не мог полноценно использовать несколько ядер. В TypeScript 7.0 существующую реализацию перенесли на Go. Логика проверки типов остаётся совместимой с TypeScript 6.0, но сам компилятор теперь работает как нативная программа и использует параллельную обработку активнее. Microsoft говорит об увеличении скорости примерно в 10 раз в отдельных сценариях. В реальных же проектах эффект будет зависеть от размера кодовой базы, структуры проекта, количества пакетов, сложности типов и ресурсов машины. Режим наблюдения за файлами tsc --watch тоже переписали. В TypeScript 7.0 он вдохновлён сборщиком зависимостей Parcel и перенесён на Go. Microsoft говорит, что простые решения на опросе файловой системы были слишком дорогими для крупных проектов. И новый наблюдатель должен стабильнее работать и меньше нагружать систему. Ограничения TypeScript 7.0 уже можно проверять в проектах, но стабильного программного API пока нет. Его обещают не раньше TypeScript 7.1. Поэтому Microsoft выпустила пакет @typescript/typescript6. Он позволяет держать TypeScript 6.0 для инструментов, которым нужен старый API, и отдельно запускать TypeScript 7.0. Нюансы при переходе TypeScript 7.0 совместим с TypeScript 6.0 по проверке типов, но конфигурация настроек обновлена и есть новые ограничения: 🔵 strict включён по умолчанию 🔵 module по умолчанию равен esnext 🔵 noUncheckedSideEffectImports включён по умолчанию 🔵 stableTypeOrdering включён и больше не отключается 🔵 rootDir по умолчанию указывает на ./ 🔵 types по умолчанию равен [] Также есть старые настройки, которые больше не поддерживаются: 🔵 target: es5 🔵 downlevelIteration 🔵 moduleResolution: node, node10 и classic 🔵 module: amd, umd, systemjs, none 🔵 baseUrl (paths можно указать относительно корня проекта) 🔵 esModuleInterop и allowSyntheticDefaultImports больше нельзя выставлять в false 🔵 alwaysStrict считается включённым и больше не отключается #frontendvkhub #go #typescript
Четыре boolean-флага isLoading, isLoaded, hasError, isSuccess порождают 16 комбинаций. Валидны шесть, остальные десять компилятор пропустит, а бизнес-логика — нет. Разрыв между типом и доменом — источник багов. Тип говорит компилятору, что все 16 комбинаций возможны. Домен говорит, что загрузка и успех одновременно невозможны. Ограничения нигде не закодированы, поэтому компилятор их не проверяет. // 16 комбинаций, 6 валидных type RequestState = { isLoading: boolean; isLoaded: boolean; hasError: boolean; isSuccess: boolean; data?: string; error?: Error; }; Тип разрешает isLoading: true и isSuccess: true одновременно. Каждый потребитель защищается условными проверками от комбинаций, которые не должны существовать: error перекрывает loading, success означает, что data существует. Это бизнес-правила, замаскированные под код. Мартин Фаулер называет это Flag Argument: boolean прячет несколько поведений за одним битом. Discriminated Unions Union кодирует инварианты в самом типе. Один литеральный дискриминант — источник правды для активного варианта: // 4 варианта, 4 комбинации type RequestState = | { status: "idle" } | { status: "loading" } | { status: "success"; data: string } | { status: "error"; error: Error }; data существует только в ветке success, error — только в error. switch (state.status) даёт точное сужение типов: компилятор знает, какой вариант активен, и запрещает доступ к полям других вариантов. Добавили "reloading" — каждый необработанный switch становится ошибкой компиляции, а не скрытым багом в рантайме. Проверка полноты function handle(state: RequestState) { switch (state.status) { case "idle": return null; case "loading": return <Spinner />; case "success": return <Data data={state.data} />; case "error": return <Error error={state.error} />; default: { const _: never = state; // ошибка при добавлении нового варианта return _; } } } Компилятор становится детектором изменений. Если добавили или переименовали состояние, то он укажет, где логика устарела. UI-компоненты — самое очевидное место для замены. Состояния часто взаимоисключающие: кнопка не может одновременно грузиться, быть отключённой из-за валидации и показывать ошибку как независимые режимы. #frontendvkhub #boolean