TGViewer
Channel Public Channel
Настя Котова // Frontend & Node.js

Настя Котова // Frontend & Node.js

@startpoint_dev

Фронтендерица с лапками 🐾
Посты каждый понедельник 💃 Копаюсь во внутрянке технологий и рассказываю вам
Subscribers
1.41K
Photos
58
Videos
0
Links
146
Recent Posts 20 shown
Post #234 484
Я к вам с новым анонсом. Этот год я заканчиваю выступлением на HolyJS! Конференция пройдёт 23–24 октября в Санкт-Петербурге и онлайн.

Мой доклад называется «Добро пожаловать на сервер: где в Next.js App Router прячутся уязвимости». Будем разбирать архитектуру App Router и смотреть, как из самого её устройства вырастают слабые места.

Как всегда, ссылки на все полезные материалы и слайды после доклада тоже опубликую тут.

А если вы планируете покупать билеты сами, то можете прийти ко мне в личку @startpoint_forl за специальными условиями.
  • ❤ 17
  • 🔥 5
  • 💅 4
Post #233 848
В прошлый раз разбирали, зачем нужны using и await using и где они работают. Теперь посмотрим, во что этот синтаксис разворачивается.

Если очень грубо, то блок с объявлениями превращается в try/finally, где в finally вызываются собранные методы очистки. Но интересно не это, а детали, которые спрятаны в спецификации.

Самое неочевидное: метод очистки берётся у объекта в момент объявления, а не при выходе из блока. Рантайм сразу достаёт [Symbol.dispose], запоминает ссылку на функцию и кладёт её во внутренний стек области. Поэтому подмена метода после объявления ни на что не влияет.


const res = { Symbol.dispose { console.log('первый'); } };
{
using r = res;
res[Symbol.dispose] = () => console.log('второй');
} // первый


Оттуда же следует и то, что если у объекта нет нужного метода, TypeError прилетит сразу на строке объявления. При этом null и undefined тоже разрешены, чтобы можно было писать необязательные ресурсы без проверок.

Теперь немного про await using. Слово await здесь относится к невидимому в коде вызову Symbol.asyncDispose при выходе из области. Поэтому в строке await using file = await open(path) два await: второй ждёт открытия файла сейчас, первый — его закрытия потом.

Если у объекта есть только синхронный Symbol.dispose, await using возьмёт его. Однако обычный using на объекте с асинхронной очисткой бросит ошибку.

Самый тонкий момент — что будет, если исключение бросит и тело блока, и сама очистка. Обе ошибки сохраняются в обёртке нового типа — SuppressedError.


try {
using r = { Symbol.dispose { throw new Error('ошибка очистки'); } };
throw new Error('ошибка тела');
} catch (e) {
e.error; // ошибка очистки
e.suppressed; // ошибка тела
}


В error лежит та, что пришла последней и вытеснила предыдущую, а в suppressed — та, которую вытеснили. Если ресурсов несколько и падают они по очереди, получается цепочка вложенных SuppressedError, по которой можно дойти до самой первой.

И последнее, про что легко забыть. Ресурс освобождается при выходе из блока, а не тогда, когда на него перестанут ссылаться. Поэтому если вернуть из функции замыкание, захватившее такой объект, оно получит уже закрытый ресурс.
  • 👍 11
  • 👨‍💻 2
  • 🔥 1
Post #232 1.19K
В JavaScript регулярно появляются новые возможности, и не про все из них мы вообще узнаём. Так, недавно я познакомилась с using и await using. Мне не довелось их применять на практике, но выглядит это интересно.

Смысл в том, чтобы привязать освобождение ресурса к границам блока. Обычно, если мы открыли файл или установили какое-то соединение, то обязаны не забыть всё это закрыть. Сейчас такой код пишется через try/finally, и выглядит примерно так:


const file = await open('data.txt');
try {
const data = await file.read();
} finally {
await file.close();
}


Здесь важно, что open стоит до try. Если убрать его внутрь блока, переменную придётся объявлять снаружи через let, а в finally добавлять ?., потому что при падении open значения там ещё нет. С await using всё это пропадает:


await using file = await open('data.txt');
const data = await file.read();


Файл закроется при любом выходе из блока: при нормальном завершении, исключении, return, break или continue. Разница здесь не столько в количестве строк, сколько в том, что исчезает целый класс возможностей ошибиться.

Работает это с любым объектом, у которого есть метод Symbol.dispose для using или Symbol.asyncDispose для await using. Стандарт описывает только этот протокол, всё остальное остаётся на рантаймах. При этом переменная, объявленная через using, ведёт себя как const, то есть переприсвоить её нельзя.

С поддержкой ситуация уже вполне боевая. Предложение вошло в стандарт ES2026. Node.js поддерживает синтаксис нативно с 24-й версии, TypeScript — с 5.2. В последних версиях браузеров тоже всё работает.

Но дальше начинаются нюансы, из-за которых этот синтаксис так редко встречается в реальном коде. Самая главная причина в том, что встроенных объектов, реализующих протокол, пока очень немного. В Node есть FileHandle из fs/promises, так что тот самый пример выше работает нативно. Однако несмотря на то, что постепенно появляются новые disposable-объекты, список всё ещё короткий.

В браузерных API встроенных disposable-объектов практически нет. Поэтому на клиенте using — это инструмент для своих объектов, а не для чужих, например:


function observe(el, cb) {
const ro = new ResizeObserver(cb);
ro.observe(el);
return { [Symbol.dispose]: () => ro.disconnect() };
}


Второй нюанс касается сборки. TypeScript понимает синтаксис давно, но при target ниже ES2022 он требует, чтобы Symbol.dispose существовал в рантайме или чтобы использовался полифил.

И третье: в обычном продуктовом коде ресурсов, которые нужно явно освобождать, не так уж и много. На фронтенде большая часть работы — это данные и рендер, а там их чистит сборщик мусора. На бэкенде файлы и соединения обычно уже спрятаны внутрь библиотек, которые закрывают всё сами. Так что реальные кейсы для применения — это скрипты, утилиты и собственные абстракции над ресурсами.

Поэтому лично мой вывод: вещь полезная, но не универсальная. Её стоит держать в голове, но области применения пока ограничены.
  • 👍 30
  • 🔥 3
  • ❤ 2
  • 💅 2
Post #231 1.29K
В цикле про рендеринг мы разбирали конвейер: стили, лейаут, отрисовка, композитинг. Всё это браузер делает для всего документа сразу, не разбираясь, видит ли пользователь эту часть страницы. На длинной ленте или в большой таблице это значит, что основной поток считает геометрию для того, до чего в этой сессии пользователь может так и не доскроллить.

Свойство content-visibility: auto позволяет эту работу отложить. Элемент с таким значением, пока он далеко от вьюпорта, не рендерит своё содержимое, а когда пользователь подбирается ближе, браузер рендерит его вовремя. Важно, что узлы остаются в DOM, ничего не удаляется и не пересоздаётся, в отличие от виртуализации списка на JavaScript.


.card {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}


Пропущенный элемент с точки зрения лейаута пустой, с нулевой высотой, и без подсказки о размере скроллбар начнёт прыгать. Поэтому необходим параметр contain-intrinsic-size, который здесь указывает ширину и высоту элемента. На практике важна лишь высота, так как ширину блочный элемент всё равно получает от родителя.

Ключевое слово auto, стоящее внутри contain-intrinsic-size перед длиной, делает оценку одноразовой: как только элемент хоть раз отрисовался по-настоящему, браузер запоминает реальный размер и дальше использует его вместо указанных 600 пикселей. Запомненный размер живёт в памяти страницы, поэтому при перезагрузке расчёт начинается заново.

Частый страх про эту механику — что она сломает поиск по странице и доступность. С content-visibility: auto этого не происходит: содержимое остаётся в дереве доступности, Ctrl+F находит текст и скроллит к нему, Tab доводит фокус до элементов внутри, и во всех случаях блок рендерится по дороге. А вот content-visibility: hidden действительно делает содержимое недоступным ни поиску, ни фокусу.

Зато есть другой, менее очевидный побочный эффект. Пока блок за экраном, его стили не вычислены, поэтому вложенные элементы, которые мы прячем через display: none или visibility: hidden, браузер пока считает обычными и показывает в дереве доступности. Если там лежит что-то служебное, его стоит закрывать через aria-hidden="true", а не только через CSS.

Поддержка у content-visibility: auto с сентября 2024 года, а в браузерах без неё свойство просто игнорируется и страница рендерится как раньше, так что фоллбэки не нужны.
  • 🔥 15
  • ❤ 4
  • 💅 4
  • 👍 3
  • 😁 1
Post #230 1.51K
Весной мы обсуждали работу с сырыми данными: ArrayBuffer, Buffer в Node.js, SharedArrayBuffer и Blob. Но кое-какие инструменты мы тогда не обсудили, а именно TextEncoder и TextDecoder, пару, которая переводит строки в байты и обратно.

API у них достаточно простое: у одного есть encode(), у другого decode(). Однако своих нюансов в их работу добавляет то, что строки в JavaScript хранятся в UTF-16, а данные вокруг (файлы, сеть и т.д.) почти всегда в UTF-8. Поэтому TextEncoder умеет кодировать только в UTF-8, и других вариантов у него нет, а вот TextDecoder принимает десятки кодировок.

Дальше начинаются суррогатные пары. Символы выше U+FFFF (эмодзи, редкие иероглифы, некоторые математические знаки) не помещаются в один 16-битный код и хранятся как два. Отдельного внимания заслуживает случай, когда суррогат остался без пары, например, строку обрезали ровно посередине символа. Тогда TextEncoder заменит его на U+FFFD (тот самый знаменитый �) и не выбросит никакого исключения.

Дело в том, что в спецификации encode() принимает USVString — строку, в которой суррогатов не бывает по определению. Обычные JS-строки — это DOMString, и при передаче аргумента происходит конвертация одного в другое, при которой каждый одинокий суррогат и заменяется. Таким образом до самого кодировщика битая строка уже не доходит.

Похожая тихая механика, без всяких ошибок, есть и у BOM — метки порядка байтов U+FEFF, которую любят добавлять в начало файла редакторы под Windows. По умолчанию TextDecoder её срезает, и в результирующей строке она не появляется. Опция ignoreBOM работает противоположно тому, что можно предположить по названию: она означает не «игнорировать метку», а «не обрабатывать её специальным образом», то есть оставить в строке как обычный символ.

Последний нюанс касается потокового чтения. Если данные приходят чанками, многобайтовый символ вполне может оказаться разрезанным на границе. Стандартный вызов декодера посчитает такую последовательность некорректной и выдаст всё тот же символ �. Однако если передать { stream: true }, то декодер сохранит незавершённый хвост до следующего вызова, а финальный decode() без аргументов сбросит остаток.


const decoder = new TextDecoder();
decoder.decode(chunk, { stream: true });
decoder.decode();
  • 🔥 11
  • 💅 5
  • 👍 3
Post #229 1.81K
Когда я готовила материал для цикла про Next.js, то неожиданно для себя узнала, что в нём есть встроенный MCP, который dev-сервер поднимает сам по умолчанию.

Начиная с 16-й версии next dev отдаёт MCP-эндпоинт по адресу /_next/mcp, настраивается это поведение флагом experimental.mcpServer. То есть прямо сейчас у любого запущенного на 16-й версии npm run dev этот эндпоинт открыт.

Внутри обычная middleware ловит всё, что начинается с /_next/mcp, и передаёт запрос в McpServer. Он живёт ровно там же, где HMR, так что в проде его не существует.

Тулов сейчас девять: get_project_metadata, get_routes, get_errors, get_page_metadata, get_logs, get_server_action_by_id, get_request_insights (требует experimental.requestInsights), плюс get_compilation_issues и compile_route — только для Turbopack.

Интересное устройство у get_errors. MCP-клиент дёргает тул → dev-сервер генерит request id и шлёт HMR-сообщение в открытые вкладки → браузер отдаёт состояние своего error overlay → ответ возвращается по тому же каналу → сервер накладывает source maps и складывает с глобальными ошибками инстанса. Поэтому для его полноценной работы требуется открытая вкладка в браузере. Тул get_page_metadata работает так же.

Благодаря такому MCP агент может сам спрашивать у живого приложения на Next.js, какая ошибка сейчас на странице, какие есть роуты и т.д. Я ещё не успела воспользоваться им в полной мере, но для задач вроде обновления сборки или большого рефакторинга, где меняется рендеринг, думаю, это может быть очень полезно.

Стоит помнить, что это неаутентифицированный эндпоинт, который отдаёт структуру проекта, пути к файлам и логи. Поэтому если вы по каким-то причинам пробрасываете dev-сервер наружу, то наружу уезжает и он.
  • ❤ 11
  • 👍 5
  • 🔥 3
  • 💅 1
Post #227 2.09K
На прошлой неделе я читала студентам лекцию про настройку инфраструктуры и рассказывала в том числе про кэширование слоёв при сборке Docker-образа.

Как всегда, стало интересно, как это работает под капотом?

Сборкой занимается BuildKit — компонент Docker, который исполняет Dockerfile. Он преобразует инструкции в граф операций и для каждой вычисляет ключ кэша. Ключи образуют цепочку: ключ текущей операции считается из её описания и ключа предыдущей. Если это COPY, к ним добавляется контрольная сумма копируемых файлов. Например, изменение базового образа меняет ключ FROM, вслед за ним — ключ следующего RUN и так далее.

Если подходящий ключ уже есть в кэше, операция не выполняется, а BuildKit использует сохранённый результат. Если ключ изменился, операция выполняется заново. Следующие операции тоже получают другие ключи, поскольку зависят уже от нового результата.

Сам результат хранится как снимок файловой системы после операции. Локально снимки и служебные записи находятся во внутреннем хранилище конкретного builder-а: например, в Docker Desktop это хранилище внутри его Linux VM.

В CI всё зависит от runner-а. На постоянном runner-е локальный кэш может пережить сборку, а на одноразовом исчезнет вместе с машиной. Поэтому в CI кэш обычно явно экспортируют в registry или хранилище CI через --cache-to, а перед следующей сборкой импортируют через --cache-from.

Именно поэтому в Dockerfile для Node.js сначала копируют package.json и lock-файл, затем выполняют npm ci и только после этого копируют исходники. Изменение кода в таком случае не инвалидирует слой с зависимостями. То есть порядок инструкций в Dockerfile определяет не только результат сборки, но и то, какую её часть придётся повторить при следующем запуске.

Ну а последняя часть цикла про Next.js будет на следующей неделе)
  • 👍 25
  • 💅 3
  • ❤ 2
Post #225 2.1K
Потихоньку подходим к завершению цикла про Next.js (осталось немного!), и теперь на очереди сборка: Webpack, Turbopack, SWC и не только — что это такое, как оно всё работает и с чем его едят.

Next.js изнутри. Часть 7. Сборка.
  • ❤ 19
  • 💅 2
  • 👨‍💻 1
Post #224 2.14K
А вот и моя самая любимая (или нет)) вещь в приложениях на Next.js — кастомный сервер! Посмотрим, что это такое, как он настраивается, и я даже поделюсь историей из своего рабочего проекта.

Next.js изнутри. Часть 6. Кастомный сервер.
  • 🙏 6
  • ❤ 5
  • 💅 2
Post #221 2.28K
На очереди App Router, и материал получается такой плотный, что я решила разбить его на две части. В первой посмотрим на ключевые концепции и разберём момент первого рендеринга, без клиентской навигации и server actions.

Next.js изнутри. Часть 3. App Router: от запроса до гидратации.
  • 🕊 9
  • ❤ 5
  • 💅 5
  • 🔥 3
  • 👍 2
Post #220 2.26K
Продолжаем погружаться в Next.js. Сегодня разбираем Pages Router: что создаёт next build, как сервер рендерит HTML при первом заходе и что происходит при клиентской навигации.

Next.js изнутри. Часть 2. Как работает Pages Router.
  • ❤ 18
  • 👍 4
  • 💅 4
Post #219 2.36K
А вот и обещанный новый цикл про Next.js под капотом.

И в первой части разберём, из каких слоёв состоит Next.js как система, как они связаны друг с другом и почему архитектура стала именно такой.

Next.js изнутри. Часть 1. Архитектура Next.js.
  • ❤ 32
  • 🔥 8
  • 💯 2
  • 😐 1
  • 💅 1
  • 🤪 1
  • 💊 1
Post #218 2.36K
Недавно на рабочем проекте я обновляла Next.js с 12-й на 16-ю версию. Далось непросто, так как были кастомный сервер на NestJS и связка через пакет nest-next, который последний раз обновлялся три года назад. Больно и неприкольно, но мы справились 💪

Этот опыт вдохновил меня наконец заняться тем, что я так долго откладывала — сходить в отпуск. Ну и написать цикл статей про Next.js)

Так что впереди нас ждёт трёхнедельный перерыв на канале, а после него — новый цикл, не переключайтесь!
  • ❤ 50
  • 🔥 16
  • 👍 6
  • 🙏 1
  • 💯 1
  • 💅 1
Post #217 2.56K
Продолжая тему с небольшими оптимизациями в браузере — сегодня поговорим про заголовок stale-while-revalidate.

Полностью он используется так:
Cache-Control: max-age=600, stale-while-revalidate=30

Что здесь происходит:
- 0–600 сек — ресурс свежий, отдаётся из кэша
- 600–630 сек — ресурс устарел, но браузер всё равно отдаёт его мгновенно, а в фоне идёт за новой версией
- после 630 сек — кэш полностью стух, пользователь ждёт

Ключевой момент: пользователь, попавший в stale-окно, видит старую версию. Фоновый запрос незаметно обновляет кэш, и уже в следующий раз посетитель получит свежие данные.

Где его используем, а где нет?
- Нехешированная статика (`/logo.png`, `/fonts/custom.woff2`) — используем, потому что URL не меняется при обновлении файла.
- API-ответы и HTML, которые меняются нечасто (каталог, лендинг, результаты поиска) — тоже используем, это даёт мгновенный ответ, а данные отстают максимум на пару минут.
- Хешированная статика (`main.a3f8c2.js`) — не имеет смысла использовать stale-while-revalidate, тут правильнее будет immutable, потому что файл по этому URL с хэшом не изменится никогда.
- Критичные данные (баланс, цена, статус заказа) — тут уже нужен no-cache.

И да, паттерн SWR знаком многим по React-библиотекам (swr, React Query), но лично я раньше не задумывалась о том, что это буквально тот же принцип, только взятый из HTTP.
  • ❤ 22
  • 👀 1
  • 💅 1
Post #216 2.14K
Что такое back/forward cache (bfcache)

Когда пользователь нажимает «назад» или «вперёд», браузер может не загружать страницу заново, а достать из памяти полный снимок: DOM, JS-кучу, состояние скролла — всё. Страница замораживается при уходе и размораживается при возврате. Переход ощущается мгновенно, потому что это не навигация в привычном смысле — это restore.

Браузер делает эту оптимизацию автоматически, но страница должна соответствовать определённым условиям. Она не попадёт в bfcache, если:

- есть listener на unload
- открыт WebSocket
- на документе стоит Cache-Control: no-store
- есть незавершённая `IndexedDB`транзакция
- и другие причины

Полный список можно посмотреть на MDN.

Диагностировать всё это можно прямо в DevTools: вкладка Application → Back/forward cache. Там можно протестировать, попадает ли страница в bfcache, и если нет — увидеть конкретный список блокеров.

Для продакшена есть программный способ — PerformanceNavigationTiming.notRestoredReasons. Через него можно собирать данные об использовании bfcache в RUM-метриках и понимать, что ломает кэш у реальных пользователей.

По итогу bfcache — один из самых «дешёвых» способов ускорить воспринимаемую производительность. Ничего не нужно дополнительно оптимизировать — достаточно не ломать нативное поведение браузера.
  • 🔥 38
  • ❤ 4
  • 👍 4
  • 👏 1
  • 💅 1
Post #215 2.16K
Почему JSON.parse() может быть быстрее объектного литерала

Казалось бы, const config = {a: 1, b: 2, c: 3} — это самый прямой способ создать объект. Но если объект достаточно большой (от ~10 KB), JSON.parse('{"a":1,"b":2,"c":3}') окажется быстрее. На бенчмарке от GoogleChromeLabs на файле в 7 МБ JSON.parse оказался в 1.7× быстрее объектного литерала в V8, в Safari разница доходила до 2×.

Причин несколько. Во-первых, грамматика JSON тривиальна по сравнению с JS — у движка для неё отдельный, более простой и быстрый парсер. Объектный литерал — это полноценный JS-код, который проходит весь путь: токенизация → AST → байткод. Так же, как описано в блоге V8, большие объектные литералы могут парситься дважды — сначала при preparsing, потом при lazy-parsing. Строка внутри JSON.parse этой проблемы лишена.

В реальном кейсе с SSR-приложением на Redux такая замена дала улучшение Lighthouse-скора с 87 до 95 и снижение TTI на 0.7 секунды. Оптимизация актуальна и в 2026 году — фундаментальные причины никуда не делись, а V8 продолжает активно инвестировать в производительность JSON (например, недавний двукратный прирост JSON.stringify в V8 v13.8).
  • 🤯 30
  • 🔥 9
  • ❤ 3
  • 👍 2
  • 💅 1
Older posts →

About this channel

How can I read @startpoint_dev without a Telegram account?
TGViewer shows the public web preview Telegram publishes for Настя Котова // Frontend & Node.js: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does Настя Котова // Frontend & Node.js have?
Настя Котова // Frontend & Node.js (@startpoint_dev) has 1.41K subscribers on Telegram, refreshed roughly every 30 minutes.
Does Настя Котова // Frontend & Node.js know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →