Настя Котова // Frontend & Node.js
1.23K subscribers
48 photos
3 files
134 links
Фронтендерица с лапками 🐾
Посты каждый понедельник 💃 Копаюсь во внутрянке технологий и рассказываю вам
Download Telegram
Сегодня день начался не с кофе, а с внепланового обновления React и Next.js 💃

Если вы пропустили, вчера была исправлена критическая уязвимость в Rect 19 и Next.js 15, связанная с серверными компонентами. Также затронуло ещё несколько пакетов.
Для исправления достаточно обновить patch-версии затронутых библиотек на своём проекте.

Подробнее можно почитать в блоге React — https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
Please open Telegram to view this post
VIEW IN TELEGRAM
2👍1🔥1
Наконец закончила вторую часть про Vite! Больше всего конечно посвятила разбору его работы в dev-режиме, а так же Hot Module Replacement.

Как работают сборщики: Vite
🔥19👍8😁1
Уязвимости React — причины и выводы

За последние пару недель вокруг React Server Components и server actions вскрылся целый пласт уязвимостей (вот и вот). В чём же была проблема?

Server actions выглядят в коде как обычные коллбеки: мы пишем функцию с use server, вызываем её из компонента — и всё. Но под капотом это POST-запрос на сервер. Клиент отправляет payload, сервер его десериализует, понимает, какой серверный модуль нужно вызвать, и выполняет этот код уже на своей стороне. И вот именно в этом месте была самая первая, самая критичная уязвимость.

В первой версии сервер доверял данным из запроса слишком сильно. Имя функции, которую нужно вызвать, приходило от клиента и использовалось напрямую для поиска экспорта в модуле. Проверки, что это действительно ожидаемый server action, что это собственное свойство модуля, а не что-то из прототипа, были недостаточно строгими. В итоге получалось, что сервер брал недоверенные данные и использовал их для выбора и выполнения кода. Это и привело к возможности удалённого выполнения кода без авторизации.

После того как эту дыру закрыли, стало понятно, что проблема глубже, чем просто имя модуля. Даже если код больше нельзя выполнить, сервер всё равно принимает сложный payload, парсит его, резолвит модули, оборачивает всё в промисы и пытается корректно обработать ошибочные сценарии. И этим тоже можно было воспользоваться: появились варианты запросов, которые не приводили к выполнению кода, но заставляли сервер долго и дорого думать, зацикливаться на обработке промисов и фактически ложиться по CPU. Так появилась DoS-уязвимость: если нельзя выполнить код, можно попытаться сломать сервер через его внутреннюю логику.

Третья проблема выросла из того же корня. В процессе обработки некорректных server action payload’ов сервер иногда формировал ответы или ошибки, в которых оказывалось слишком много внутренней информации. В отдельных сценариях это позволяло получить исходный код серверных функций или детали их реализации. Код не выполнялся, сервер не падал, но граница между «внутренним» и «внешним» снова оказывалась размыта.

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

Всё это, на мой взгляд, хороший повод ещё раз подумать о том, что в момент, когда мы как фронтенд-разработчики выходим на сервер, правила немного меняются. Фронтенд уже следит за большим количеством вещей: UI и UX, производительность интерфейса, его состояние, кроссбраузерность и т.д. Просто теперь к этому списку всё чаще добавляются сервер, безопасность, миграции, обратная совместимость и API.

Даже на простых вещах это легко почувствовать. У нас, например, на проекте с BFF однажды возникла волна 404 во время выкатки нового релиза просто потому, что мы переименовали эндпойнты и не заложили многоступенчатую миграцию между старым клиентом и новым сервером. Никто не делал ничего «неправильно», мы просто не подумали об этом сценарии, потому что крайне редко с ним сталкиваемся. И вот такие истории с уязвимостями в React хорошо подсвечивают одну мысль: когда фронтенд становится ближе к серверу, про серверные риски тоже нужно начинать думать чаще.
🔥16💯64😱1🙏1
В следующем году я опять планирую выступать на конференциях. И в этот раз начну с DUMP в Санкт-Петербурге!

Готовлю доклад с темой Ignition, Sparkplug, Maglev, TurboFan: как компилирует V8
Он вышел по следам моего цикла статей в V8, но будет в 100 раз подробнее именно в части работы интерпретатора и компиляторов движка 💫

Презентацию и запись (если она будет) выложу сюда после конференции. Но если есть возможность — конечно же приходите лично!
Подробнее можно почитать тут
🔥181
А ещё к концу года хочу сделать подборку моих самых лучших статей за этот год. Поэтому буду признательна, если проголосуете, что именно вам понравилось больше всего!

(в опросе представлены не все посты, а только самые “жирненькие”, но всегда можно дополнить этот список в комментариях)
👏5
Вот и наступило время (да-да) для итогов 2025 года!

2025 для меня и моего канала выдался хорошим и очень продуктивным. Я нашла направление, в котором мне действительно интересно разбираться и готовить материал, выпустила несколько циклов статей и выросла со 100 подписчиков до 800+ (глядишь, и до тысячи когда-нибудь дойдём!)
А ещё — выступила на двух оффлайн-конференциях и одном онлайн-митапе 💃

Хайлайтами года хочется оставить:

🔥 лучший цикл статей, на ваш взгляд и по статистике
Event Loop в Node.js

⚙️ второе место делят циклы
→ Как работают сборщики (Webpack и Vite) и Погружение в v8

🖤 недооцененный, но один из моих любимых
Погружение в libuv

🚀 самый популярный пост (по количеству пересылок)
Про последние уязвимости в React

лучшие мемы на канале
→ все 💫

Вообще, в начале этого года одним из направлений я выбрала развитие личного бренда. Мне хотелось больше писать и выступать, набирать аудиторию и параллельно прокачивать экспертизу в интересных мне темах. Мир фронтенда бездонный для изучения, но 3–4 года до этого я будто законсервировалась в своих знаниях. Будем честны: обычная работа среднестатистического разработчика не подкидывает тебе постоянно новые технологии и неординарные задачи. А искать что-то самому без чёткой цели или выхлопа работает не для всех.

Для меня этот канал, эти статьи и посты стали мотивацией к регулярному развитию. Весь год я дисциплинировала себя — каждую неделю должна была изучить что-то новое, разобраться в чём-то и рассказать об этом здесь, для вас. И я рада, что такая системность приносит свои плоды (хотя бы в растущем числе подписчиков).

Неизвестно, что будет с индустрией дальше. Может, я и правда поздно залетела во всю эту историю с прокачкой личного бренда. Но как минимум одно точно мотивирует меня продолжать — в декабре 2025-го я знаю гораздо больше, чем в декабре 2024-го. И я не узнала бы и половины из этого, если бы не этот канал. Так что продолжаем!

Ну а пока я ухожу на небольшие новогодние каникулы здесь, чтобы все мы немного отдохнули. Новый материал, как обычно, ждите 12 января!)
С наступающим! 💫
136🔥8👍1👏1
Этот год здесь хочется начать с какого-то не перегружающего поста, потому что, кажется, многие всё ещё отходят от новогодних праздников (я так точно) и только потихоньку въезжают в рабочий режим.

Про ИИ в разработке уже не написал разве что ленивый. В частности, про его использование как рабочего инструмента. Я не буду открывать Америку, а просто поделюсь своим небольшим опытом — вдруг кому-то окажется актуально.

Я не считаю себя ни ярым сторонником, ни противником ИИ. Не согласна с тем, что он заменит всех, но и игнорировать его возможности и плюсы тоже странно. Поэтому на работе я уже несколько месяцев активно использую Cursor. Я всегда работала в VS Code, так что перейти на него было совсем несложно. И вот пара вещей, которые помогли мне приспособить его под себя.

1. Memory Bank для каждого проекта

Сейчас мы работаем сразу над несколькими фронтенд-проектами, и для каждого из них у нас заведён свой Memory Bank. Это специальная папка, где в Markdown-файлах описаны основные правила работы с кодом: от codestyle до архитектуры проекта и примеров реализации популярных задач.

Чтобы Cursor всегда учитывал эту информацию, мы настроили project rules в .cursor/rules и явно указали, где именно лежит Memory Bank. Если вы используете несколько ИИ-тулов, такая папка может быть общей — а дальше каждая тулза просто будет на неё ссылаться.

Инициализировать Memory Bank довольно просто: достаточно попросить Cursor проанализировать проект и сгенерировать черновой вариант. Мы так и сделали, а потом тщательно всё отревьюили (потому что, справедливости ради, в таких генерациях он любит иногда придумывать).

После появления Memory Bank полезность рекомендаций ИИ заметно выросла, по крайней мере, по моим ощущениям. Плюс это избавляет от постоянного повторения одного и того же и одинаковых промптов в духе «сделай аналогично вот этому».

2. Реализация больших фичей через поэтапное планирование

Если с какими-то очень специфичными багами ИИ меня скорее тормозит, чем помогает, то вот при добавлении нового функционала с нуля он может быть отличным помощником. Особенно когда нужно написать сразу несколько компонентов, стили для них, логику в стейт-менеджере и реализовать новые запросы на бэкенд.

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

Дальше в этом же промпте я прошу ИИ не переходить к реализации, а сначала составить подробный план работ: что именно он будет делать и куда вносить изменения. Я делаю ревью плана, обычно хватает 1–2 итераций правок, чтобы исправить неточности или недопонимания. После этого план полностью готов, и я прошу реализовать его либо целиком, либо поэтапно, если в задаче есть неопределённость.

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


На самом деле именно эти две вещи я бы назвала фундаментальными в своей работе с Cursor. Всё остальное — это скорее нюансы и точечные улучшения, вроде использования MCP-серверов. Но в целом ИИ и такие IDE, как Cursor, действительно ускоряют работу и снимают с меня рутинную часть, оставляя мне главное — продумывать реализации и дошлифовывать результат до идеала.
👍21🔥51
Новый год — новый цикл статей!

За последнее время мы обсуждали и V8, и какие-то нюансы работы Node.js. Теперь же пришло время прикоснуться к самому прекрасному во фронтенде — к рендерингу 💫

От кода до пикселей: как работает рендеринг. Часть 1. Браузерные движки.

Первая часть уже ждёт вас 💃
131👍11❤‍🔥5🔥3
Вторая часть цикла уже здесь. Начинаем разбирать конвейер рендеринга на примере Chromium (спойлер: там не всё так просто, как было раньше) 🙊

От кода до пикселей: как работает рендеринг. Часть 2. Конвейер рендеринга.
12👍4🔥1
Продолжаем разбирать конвейер рендеринга на примере Chromium в третьей части цикла. Посмотрим подробнее на первые 8 стадий.

От кода до пикселей: как работает рендеринг. Часть 3. Стадии основного потока.
11👍2
Наконец заканчиваем мучать несчастный конвейер рендеринга в Chromium! В четвёртой части цикла рассматриваем оставшиеся 5 этапов.

(спойлер-спойлер: это не всё, что мне хочется посмотреть в теме рендеринга, так что цикл не закончен, не переключайтесь)

От кода до пикселей: как работает рендеринг. Часть 4. Стадии композитного потока и работа с GPU.
11
Моё выступление на питерском DUMP уже завтра!

Но не грустите, если вас там не будет! Я, как и в прошлые разы, выложу сюда слайды и материалы. А также запись самого выступления, как только она появится 💫
111🔥6👍2
Компиляторы в V8.pdf
25.4 MB
Пока я отдыхаю после активной недели в Питере, отправляю обещанные материалы и слайды моего пятничного доклада про компиляторы в V8.

Материалы - https://github.com/startpointforl/v8-compilers-materials

А цикл про рендеринг вернётся уже на следующей неделе.
1🔥11
Отойдём немного от браузера и посмотрим на то, что нам ближе и роднее. А именно, на рендеринг в React.
Что интересного сделали разработчики React, чтобы оптимизировать отрисовку наших компонентов? Уже в новой части цикла!

От кода до пикселей: как работает рендеринг. Часть 5. Рендеринг в React.
10👍25🔥32
Пока я болею весенней простудой, запланированная последняя часть цикла про рендеринг ждёт вас.
Как необъятен мир рендеринга, сколько всего интересного скрывается в нём, не только со стороны обычного HTML, но и в видео, SVG и даже PDF! Чтобы разобраться в каждой из тем глубже потребуются отдельные циклы статей, но посмотреть верхнеуровнего мы можем уже сейчас.

От кода до пикселей: как работает рендеринг. Часть 6. Альтернативные пути рендеринга.
🔥9👍21
Неожиданно для самой себя в эту среду, 18 марта в 19:00 по Мск, я буду читать доклад про V8 на открытой сессии Podlodka React Crew.
Буду больше рассказывать в том числе про Hidden classes и Inline Caches — то, на чём строятся оптимизации вокруг объектов внутри движка V8.

Приходите, трансляция на открытую сессию будет доступна без регистрации 💫
🔥26
Я никогда глубоко не работала с бинарными данными в JavaScript. Какое-то время назад стало интересно, что же там есть, как оно работает и для чего используется. Дальше в нескольких постах будет мой микроконспект, а также некоторые интересные нюансы работы "под капотом".

ArrayBuffer — это основа всего, просто кусок сырой памяти. Однако работать с ним напрямую нельзя. Для этого существуют TypedArray. Это набор вьюшек поверх ArrayBuffer с конкретной интерпретацией байтов, например:


const buf = new ArrayBuffer(16);
const i32 = new Int32Array(buf); // 4 элемента по 4 байта
const u8 = new Uint8Array(buf); // 16 элементов по 1 байту


Здесь команда new ArrayBuffer(16) выделяет непрерывную область памяти размером 16 байт и заполняет её нулями, а обе вьюшки смотрят на одну и ту же память.

Доступных вьюшек много, вот они: Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array. По факту все они позволяют отобразить выделенную память в числа разного диапазона и количества байт.

Есть ещё DataView, который позволяет читать разные типы с произвольных смещений:


let dataView = new DataView(buf);
dataView.getUint8(0); // получим 8-битное число на позиции 0
dataView.getUint32(0) // получим 32-битное число на позиции 0
dataView.setUint32(0, 0); // установим 4-байтового числа в 0 на позиции 0


Что происходит под капотом? Из интересного, сырые байты ArrayBuffer, если он больше 64 байт, V8 хранит за пределами кучи. Давайте разберём этот механизм подробнее.

Когда вы создаёте new ArrayBuffer(1024), V8 внутри себя создаёт два объекта:

1. JS-объект ArrayBuffer — живёт в куче, участвует в сборке мусора
2. BackingStore — обёртка вокруг указателя на реальный блок памяти

BackingStore знает три вещи: указатель на данные, размер и как эту память освободить. Когда ArrayBuffer собирается GC, BackingStore вызывает деаллокатор внутри V8.

Для маленьких ArrayBuffer (≤ 64 байт) V8 может хранить данные прямо внутри JS-объекта в куче. BackingStore как структура формально существует, но отдельной аллокации памяти за ним нет.

С одной стороны, если бы ArrayBuffer всегда лежал внутри кучи, то память быстрее бы аллоцировалась. Однако V8 не держит большие буфферы там, потому что для использования в postMessage(arrayBuffer) разработчики ожидают zero-copy передачу, которую нельзя позволить из соображений безопасности, если ArrayBuffer хранится в куче. Обсуждение этого можно почитать тут.

И ещё один факт, если вы помните про ElementKinds из моей статьи про V8, то там мы разбирали базовые типы элементов, которые используются чаще всего.
Elements Kinds — внутренние категории V8, определяющие, как именно хранятся элементы массива в памяти и как к ним обращаться.

Однако для TypedArray у движка есть отдельные ElementKinds, причём каждый тип (Int8Array, Float64Array и т.д.) — это отдельный ElementKinds. Всё потому, что для TypedArray тип всегда зафиксирован при создании, в то время как для обычных массивов типы могут меняться в рантайме.
15👍4🔥1
Продолжаю про работу с сырыми данными. В этот раз — про Buffer.

Buffer — это Node.js-специфичная вещь, в браузере его нет. Технически он наследуется от Uint8Array, то есть это тот же TypedArray, но с дополнительными методами для удобной работы со строками и файлами:


const b = Buffer.from('hello', 'utf8');
b.toString('hex'); // '68656c6c6f'


У Buffer есть три статических метода для аллокации, и они работают принципиально по-разному.

Buffer.alloc(size) — безопасный вариант. Создаёт отдельный ArrayBuffer нужного размера, память заполняется нулями. Пул не используется.


Buffer.alloc(8); // <Buffer 00 00 00 00 00 00 00 00>


Buffer.allocUnsafe(size) — быстрый вариант. Память не обнуляется, в буфере могут быть остатки старых данных. Но самое интересное — это то, как он выделяет память.

Buffer.allocUnsafeSlow(size) — как allocUnsafe, но всегда создаёт отдельный ArrayBuffer, без пула. Зачем — объясню ниже.

Когда вы вызываете Buffer.allocUnsafe(100), Node.js не идёт каждый раз просить систему выделить 100 байт. Вместо этого он заранее выделяет один большой кусок памяти на 8 КБ (пул) и нарезает от него маленькие буферы:

Если запрошенный размер меньше 4096 байт (половина от размера всего пула), Node.js просто двигает указатель внутри пула и создаёт новый view на уже существующий ArrayBuffer. Новой аллокации памяти нет, поэтому это очень быстро.

Если в текущем пуле не хватает места — создаётся новый. Если запрошенный буфер слишком большой (≥ 4096) — пул не используется, создаётся отдельный ArrayBuffer.

Порог в половину пула выбран не случайно: если буфер занимает больше половины, он съест почти весь пул, а оставшийся хвост станет бесполезным.

После каждой нарезки poolOffset выравнивается до границы в 8 байт. Это нужно для производительности: невыровненный доступ к памяти на некоторых архитектурах медленнее.

Все маленькие буферы, нарезанные из одного пула, разделяют один ArrayBuffer. Это значит: пока хотя бы один такой буфер жив, все 8 КБ пула не могут быть собраны GC.

Именно для этого существует Buffer.allocUnsafeSlow(size) — он всегда создаёт отдельный ArrayBuffer. Если вы создаёте много маленьких буферов и удерживаете их надолго (кешируете, складываете в массив), allocUnsafeSlow может быть экономичнее по памяти.
9👍52🔥1
Продолжаю серию. В этот раз — про SharedArrayBuffer и историю с отключением во всех браузерах.

SharedArrayBuffer — почти то же самое, что ArrayBuffer, но с одним принципиальным отличием: его можно передать в несколько Web Workers одновременно, и все они будут работать с одной и той же памятью.


const sab = new SharedArrayBuffer(256);
worker.postMessage(sab); // worker получает доступ к той же памяти


С обычным ArrayBuffer при передаче через postMessage происходит transfer — оригинал становится недоступным, а получатель получает владение. Это сделано намеренно: два потока не должны одновременно писать в одну память без синхронизации. SharedArrayBuffer снимает это ограничение — но тогда синхронизацию нужно делать самостоятельно. Для этого существует объект Atomics. Он предоставляет атомарные операции — гарантированно неделимые чтение-запись:


const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);

// В воркере 1:
Atomics.add(view, 0, 1); // атомарный инкремент

// В воркере 2:
Atomics.load(view, 0); // атомарное чтение


Без Atomics параллельная запись в SharedArrayBuffer приводит к data race — результат непредсказуем. Atomics.add, Atomics.load и другие методы гарантируют, что операция выполнится целиком, без прерывания другим потоком. Есть и механизм ожидания: Atomics.wait(view, index, expectedValue) блокирует поток, пока значение по индексу равно ожидаемому. Atomics.notify(view, index) будит ждущие потоки.

У SharedArrayBuffer непростая история. В начале 2018 года его отключили во всех браузерах — Chrome, Firefox, Safari, Edge — одновременно. Причина: атака Spectre. Чтобы понять, при чём тут SharedArrayBuffer, нужно разобрать саму атаку — она на самом деле довольно изящная.

Современные процессоры не ждут, пока проверится условие if — они предсказывают результат и выполняют код «наперёд». Если предсказание верное — результат уже готов. Если нет — процессор откатывает результат. Но есть нюанс: данные, которые попали в кеш процессора во время спекуляции, остаются там даже после отката.

Допустим, в памяти процесса (рядом с нашим JS-кодом) лежит секретный байт — например, кусок данных от другой вкладки. Напрямую прочитать его нельзя, есть проверка границ. Но атакующий может обойти её через спекуляцию.

Шаг 1: атакующий много раз вызывает код с валидным индексом. Предсказатель ветвлений запоминает: «условие всегда true».
Шаг 2: атакующий подаёт индекс, который выходит за границу массива и указывает на секретный байт. Процессор по привычке предсказывает «true» и спекулятивно читает секрет — допустим, значение 42.
Шаг 3: спекулятивный код использует прочитанное значение как индекс в другом массиве — probeArray[42 * 256]. Эта ячейка попадает в кеш. Потом процессор понимает, что условие было false, и откатывает всё. Но кеш не откатывается.
Шаг 4: атакующий перебирает все 256 возможных значений, обращаясь к probeArray[0 * 256], probeArray[1 * 256], …, probeArray[255 * 256], и замеряет время каждого обращения. Одна ячейка отвечает за ~3 наносекунды (из кеша), все остальные — за ~100 наносекунд (из оперативной памяти). Быстрая ячейка выдаёт значение секрета: 42.

Секрет утёк не напрямую, а через побочный канал: значение было использован как индекс в массиве и он оставил физический след в кеше.

Вся атака стоит на способности различить «3 наносекунды» и «100 наносекунд». Для этого нужен очень точный таймер. performance.now() давал точность ~5 микросекунд — это всё ещё достаточно грубо. А SharedArrayBuffer позволяет собрать самодельный наносекундный таймер буквально в пять строк:


// Воркер-таймер:
const counter = new Uint32Array(sharedBuffer);
while (true) {
counter[0]++;
}

// Основной поток:
const start = counter[0];
// ... замеряем что-то ...
const elapsed = counter[0] - start;


Один воркер крутит счётчик в бесконечном цикле, другой поток читает его до и после интересующей операции. Разница — время выполнения с точностью вплоть до наносекунд. Этого достаточно, чтобы отличить попадание в кеш от промаха, а значит — достаточно для Spectre.
10🔥151