TGViewer
Channel Public Channel
SuperOleg dev notes

SuperOleg dev notes

@super_oleg_dev

Обзоры новостей и статей из мира frontend, интересные кейсы, исследования и мысли вслух

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Subscribers
1.75K
Photos
57
Videos
3
Links
160
Recent Posts 18 shown
Post #239 550
Далее, про скролл.

В принципе для SPA-приложений это база, если при переходах на разные страницы мы не хотим оставаться в одной позиции скролла, нам нужно самостоятельно скроллить вверх страницы.

Кажется есть функционал во всех роутерах и мета-фреймворках, и как отдельные либы.

В Tramvai был старый модуль, который умел вызвать scroll top или к якорю, с логикой что бы срабатывал строго один раз после перехода.

Из коробки проблема опять таки при браузерных back/forward, в этих случаях мы хотим восстановить позицию скролла, а не сбросить ее наверх.

И конечно кейсы с виртуальными списками / пагинацией, но это уже немного out of scope, кажется тут лучше якорей ничего нет.

Но мы словили ещё интересную проблему:
- у страницы очень тяжёлый рендер
- браузер детектит soft navigation до отрисовки и делает восстановление скролла
- наш автоскролл не попадает в этот paint и у пользователя скачет скролл на одном экране

Зеркальная проблема, если автоскролл сработал до отрисовки текущего экрана а браузер восстановил скролл после paint события, которое стриггерил реакт при рендере следующего экрана.

Я долго возился с отключением/включением браузерного механизма восстановления при разных условиях для разных записей в history, все стало работать на первый взгляд идеально, а потом я включил example с View Transitions...

Потратил ещё больше времени, но все время упираюсь в одну проблему - браузерное восстановление скролла и анимация не синхронизируются, попадают в разные paint, какие бы хаки я там не делал, включая оборачивание скролла в rAF.

Пришел к тому что единственный способ рабочий это:
- браузерный механизм отключить полностью
- сохранять позицию скролла для каждой записи в history вручную
- восстанавливать/сбрасывать ее тоже полностью вручную

Так как возможны переходы вида history.go(-2), в session storage для каждой навигации (по индексу) храню позицию скролла.

Отдельно пришлось повозиться, снова, с браузерными back/forward - о переходе узнаешь пост-фактум, history уже изменен, и как раз плюсом стало хранение позиции скролла в session storage.

Работает как часы, кастомизируется, но дебажить такое это конечно никакого удовольствия - https://github.com/tramvaijs/tramvai/blob/main/packages%2Fmodules%2Fautoscroll%2Fsrc%2Fcomponents%2FScrollRestoration.tsx
GitHub tramvai/packages/modules/autoscroll/src/components/ScrollRestoration.tsx at main · tramvaijs/tramvai A modular framework for universal JS applications. Contribute to tramvaijs/tramvai development by creating an account on GitHub.
  • 🔥 3
  • 🤔 2
  • 👍 1
Post #238 572
Раз уж меня прорвало, вспомню ещё один интересный кейс.

Решали примерно в одно время две проблемы:
- кастомизация view transitions
- восстановление скролла при переходах с VT

С view transitions кастомизацией основная трудность была в back/forward переходах через браузерные кнопки и жесты, которые роутер видит по факту события popstate.

Соответственно тут разработчик не может задать конкретный view transition type для перехода назад или вперёд, добавляются только встроенные в Tramvai types.

Но, допустим мы листаем галерею, каждый элемент это роут. Тогда нам нужна анимация слева-направо как при прямых переходах на предыдущий слайд так и при back переходах с последующих слайдов (то есть зеркальная).

В итоге получилось достаточно изящно решить проблему:
- добавлен отдельный tapable хук который возвращает VT types для текущей навигации
- возвращаемое значение разработчик может переопределить
- в параметрах хука есть вся информация - о VT предыдущего, текущего и следующего роутов, и кто инициатор - браузер или наш роутер

Пример в документации - https://tramvai.dev/docs/features/routing/view-transitions#how-to-customize-view-transition-for-browser-backforward-buttons

А с релизом React 19.3 новый челлендж - как подружить трамвайный механизм VT для роутов с нативными и декларативными апи реакта...
tramvai.dev View Transitions | tramvai Overview
  • 🔥 2
Post #237 599
Вместо тысячи слов, пример работы и сразу разметка, которая долетает в HTML - и наш механизм, и React досылает разметку suspended компонентов
  • 🔥 6
  • ❤ 5
Post #236 635
Когда-то давно рассказывал про интеграцию стриминга и deferred загрузки данных в Tramvai - https://t.me/super_oleg_dev/136

У этого механизма была проблема:

Все пользователи обычно заводят сторы для данных, и обновляют их в Actions после загрузки данных.

Но, если экшен больше не блокирует ответ пользователю, значит initial state мы уже отправили на клиент, и dispatch(event) после завершения запроса к API работает впустую.

При переходе на Deferred Actions пользователям приходилось рефакторить экшены, делать дополнительные client-only экшены с синхронизацией стейта с deferred данными из стрима.

Выглядит страшно - https://tramvai.dev/docs/features/data-fetching/streaming-data/#manual-synchronization-alternative

И наконец-то мы развили эту идею!

Я называю это следуя моде state-over-the-wire 😂

Принцип работы очень простой:
- если экшен deferred
- и первый ответ пользователю уже ушел
- тогда dispatch(event) пушит событие и данные в стрим ответа, как script в HTML
- на клиенте же, мы после гидрации начинаем обработку этих событий и автоматически обновляем сторы

Таким образом все по канонам (похожим образом в стриме разметку например присылает React), и позволяет получить все плюшки от раннего не блокирующего ответ запроса, вообще без рефакторинга кодовой базы.

Коллеги, берегитесь, будем очень активно продавать 🙈
Telegram SuperOleg dev notes Привет! Релизнули экспериментальный функционал с Deferred экшенами. Сделал темплейт где можно потыкать результат - https://codesandbox.io/p/sandbox/tramvai-deferred-actions-s95q62 Экшен выполняется 1 секунду, время рендера компонента который зависит от…
  • 🔥 5
Post #235 831
TIL, небольшой, но интересный кейс!

Делал кастомный анализ бандла через Claude, пишет мне слишком большой размер пакетов по сравнению с bundle-analyzer и Statoscope.

Набор пакетов общий по смыслу, речь идёт о 50+kb gzip кода.

Попросил копнуть глубже, и явно выделить дубликаты - показывает дубли каждого пакета!

Но:
- в локфайле все ок, пакеты по одному экземпляру
- в Statoscope все аналогично хорошо

И только потом я замечаю, что пакет один, но в бандл попали и CJS и ESM версии модулей 😭

Оказывается, один промежуточный пакет поставляется только в CJS, и затягивает поддерево CJS модулей тех пакетов, что вообще-то корректно поставляются и попадают в бандл в ESM версиях.

Вот такой вот dual package hazard, пишите в тред если знаете тулзы как такое быстро находить.
  • 🔥 13
  • 👍 3
Post #233 2.25K
Ещё из интересного, в ближайшем будущем будет поставлена точка в спорах вокруг масштабируемости подхода Vite с загрузкой модулей приложения в режиме разработки без бандлинга.

В принципе то что загрузка тысяч модулей в браузере на старте плохо масштабируется, это факт, даже при использовании HTTP/2, это было наглядно ещё при живом Snowpack.

Но в issues/тредах по теме позиция мейнтейнеров Vite зачастую в стиле - у нас все нормально, обновитесь стало ещё быстрее.

Теперь же rolldown открывает возможности бандлинга зависимостей в development режиме (как все эти наши *pack'и), что уже показывает лучший перф и в будущем это станет дефолтом - https://v7.vite.dev/guide/rolldown#why-introducing-a-full-bundle-mode
vitejs Rolldown Integration Next Generation Frontend Tooling
  • 🔥 9
  • 🖕 1
Post #232 1.74K
Не самая свежая новость, но релиз Vite напомнил про крутой эксперимент в Oxc как ускорить работу JS плагинов и не терять профит от Rust тулинга, как я понимаю идею в нескольких предложенях:
- вместо сериализации AST как json, гоняем между rust и js напрямую ссылки на участки памяти
- на стороне js работаем с Buffer
- пишем десериализатор этого AST на js который работает очень быстро так как простой и мономорфный

Нюансов конечно много, и для меня фронтендера звучит как чернокнижная магия, в любом случае очень рекомендую к прочтению - https://github.com/oxc-project/oxc/issues/2409

Также напомнило подобный подход в Deno - https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-11/
GitHub Faster passing ASTs from Rust to JS · Issue #2409 · oxc-project/oxc Currently OXC's parser is extremely fast, but using it from NodeJS is not. The primary cause is the overhead of the JS/Rust boundary - specifically serializing/deserializing large AST structure...
  • 👍 12
Post #231 1.91K
Забыл еще про один небольшой кейс в этом наборе оптимизаций.

На старте cli проверяем текущую версию yarn, а уже для v1 и для berry у нас разные обертки для работы с зависимостями.

Проверка сделана через child_process.execSync, вызываемая команда - yarn -v

Занимает этот вызов в итоге 300-600ms у v4 yarn, асинхронный exec ситуацию не исправил и как будто бы даже замедлил в сумме.

В качестве воркэраунда добавил сначала проверку на наличие поля packageManager в package.json приложения - должно быть в любом проекте с corepack.

Вроде бы все низковисящие фрукты собрал.
  • 👍 5
  • 🔥 3
Post #230 1.66K
Занимался сейчас оптимизацией времени старта @tramvai/cli, и самый популярный паттерн такой:
- снять CPU profile
- найти самые тяжелые CJS require вызовы
- если импортируемые пакеты не используются, заменить обычный import в начале файла на require(lib) по месту вызова

Буквально несколько тяжелых импортов зря занимали 500-1000ms на старте скрипта, очень эффективная и простая оптимизация, хоть и требует ручной работы + смешивать CJS + ESM.

Есть классный пропозал с defer import - https://github.com/tc39/proposal-defer-import-eval

Но проблема та же, что вручную надо определить, что нам требуется не сразу.

Плюс запустить это можно только на собранном через бандлер коде на данный момент - https://webpack.js.org/configuration/experiments/#experimentsdeferimport

Еще одна приятная оптимизация - замена require('date-fns') на require('date-fns/format'), минус 350ms - кажется бесполезный налог на жирный barrel файл - точку входа date-fns.

Следующий момент вдохновлен вебпаковским thread-loader, опишу сначала сам пайплайн dev сборки (трейс на вложенном изображении):
- наша cli запускает процессы сборки серверного и клиентского кода
- также она стартует воркер для фетча и запуска собранного server.js
- соответственно этот воркер простаивает зря все время пока собирается серверный код

Добавил на старте воркера прогрев самого тяжелого модуля - require('fastify') - итого минус 200ms на старт development сервера приложения.

Получаю большое удовольствие от такой работы, хоть теперь и не доверяю Chrome Devtools Performance как прежде :)
  • 👍 14
  • 🔥 5
Post #229 1.15K

Forwarded from Иван Акулов про разработку

Что ещё
😑 Потратил на это выходные, поэтому сэкономлю их вам:

Оказывается, когда вы открываете девтулзы в Chrome, таймеры (и любые другие нативные функции — fetch, requestAnimationFrame и т.д.) становятся в 5-100 раз медленнее. Это происходит вне зависимости от того, делаете ли вы что-то в девтулзах или нет — достаточно того, чтобы они были открыты.

Это проблема! Перформанс обычно измеряется с открытыми девтулзами. Из-за того, что таймеры в этом режиме кажутся дорогими, TanStack Query, например, имплементировал целое API, чтобы менеджить таймеры, а Сергей Гарин ушёл даже дальше и почти переписал таймер-менеджмент целиком.

Мини-тред с исследованием (где в конце приходит Пол Айриш из Google и говорит, что все мы делали всё это зря): раз, два, три
  • 👍 11
  • ❤ 4
  • 😱 2
Post #228 1.43K
Встречал на практике кейс с десятками запросов на старте, где каждый fetch занимал 2-4 синхронных миллисекунды, и в сумме это казалось прям катастрофой. А оказывается это может быть лишь оверхэд на девтулзы...
  • 👍 9
  • 😁 4
  • 🤡 3
Post #227 1.72K
Интересный драфт появился в Undici (современный встроенный в Node.js клиент для запросов) - реализация паттерна Circuit Breaker - https://github.com/nodejs/undici/pull/4700/

По сути, сейчас Undici закрывает практически все кейсы, которые мы хотим видеть для эффективных серверных запросов:
- кэширование запросов
- дедупликация запросов
- ретрай запросов
- проксирование и поддержка переменных окружения http_proxy/no_proxy
- переиспользование tcp сокетов (из коробки, а с http.request для этого нужно прокидывать явно Agent с keepAlive: true)
- dns кэширование
- circuit breaker
- мониторинг/логирование через diagnostics_channel

Мы как раз активно мигрируем на undici.fetch в Tramvai на замену node-fetch, и это уже вылилось в несколько небольших доработок в undici:
- мониторинг кэша - https://github.com/nodejs/undici/pull/4589
- мониторинг прокси - https://github.com/nodejs/undici/pull/4659
- env proxy ближе к стандарту - https://github.com/nodejs/undici/pull/4676

Кстати, классные и дружелюбные мейнтейнеры (называю их за глаза "мои итальянцы"), но уже месяца два не могу дождаться релиза - возможно готовят мажорку или просто хотят большой скоуп фичей за раз.

Так вот, наша особенность - SSR приложения требуют универсальные интерфейсы для сервера и для браузера, для этих целей у нас реализована библиотека @tinkoff/request, которая работает поверх fetch в браузере и теперь уже поверх undici.fetch в ноде.

Если посмотреть на список плагинов @tinkoff/request - https://tramvaijs.github.io/request/docs/plugins/index - мы увидем те же кэши, дедупликацию, circuit breaker (очень кстати эффективный механизм для кейсов когда в обычном виде кэширование не подходит, но можно делать фаллбэк кэши на случай сбоев), в общем все что нужно для хорошего http клиента и не завязано на конкретное окружение.

Специфичные под серверное окружение вещи (dns кэши и прочее) мы настраиваем уже на уровне отдельных Tramvai модулей.

Теперь же получается, что мы дублируем функционал Undici, а было бы очень круто переиспользовать все его возможности - но упираемся в то, что в браузере у нас обычный fetch, а у мейнтейнеров Undici в принципе планов на веб нет (и это вполне оправданно).

Кажется, было бы очень полезно если бы браузерный fetch расширяли новыми возможностями (по принципу тех же interceptors в Undici), и браузерное API поставляло бы готовые сущности для таких базовых вещей как дедупликация и ретрай запросов, мониторинг, да и наверное проксирование.

С другой стороны, многие вещи уже идут в браузере из коробки - кэширование с учетом Cache-Control, E-tag и прочих заголовков, переиспользование TCP сокета открытого под конкретных хост, а все отправленные запросы доступны через performance.getEntries('fetch').

И возможно опциональные изменения нужны именно в браузерные механизмы работы сетевых запросов, или в саму спеку HTTP протокола, где ту же дедупликацию мы могли бы настроить через новые HTTP-заголовки?
GitHub feat: add circuit breaker interceptor by mcollina · Pull Request #4700 · nodejs/undici Summary Adds a per-host/route circuit breaker interceptor for resilient HTTP client behavior. Features Three circuit states: CLOSED (normal operation), OPEN (fast-fail), HALF_OPEN (testing recover...
  • 🔥 13
  • 👍 8
  • 🤔 3
Post #225 2.81K
Привет!

Достаточно давно делился статьей где описывал различные механизмы и подходы которые мы применяем для SSR приложений на Tramvai (сейчас доступна на хабре).

Один из механизмов - Request Limiter, модуль который ограничивает количество параллельно обрабатываемых запросов при перегруженном Event Loop приложения, для возможности стабильно отдавать 2xx ответы и рендерить странички даже под большими нагрузками.

Работает по похожим принципам с https://github.com/fastify/under-pressure, только не отбрасывает все запросы при нагрузке, а держит еще LIFO очередь что бы обеспечить большее количество успешных ответов без сильной деградации времени ответа.

Когда переводили наши интеграционные тесты на 20 версию Node.js, начали падать нагрузочные тесты Request Limiter. Основная проблема - перестали быстро отвечать health-чеки приложения (а отзывчивые health-чеки и метрики очень важны и что бы в реальном времени понимать что происходить и для graceful рестартов и так далее)

Долго исследовал проблему, опытным путем обнаружил что проблема где-то на уровне http модуля ноды, запросы как будто в какой-то момент скапливаются в очередь и не обрабатываются далее.

Завел issue, получил много интересного и ценного фидбека от Matteo Collina - https://github.com/nodejs/node/issues/57364

Оказалось, что изменения в libuv убрали запуск таймеров в начале цикла на старте event loop, теперь они запускаются только в конце - то есть после poll (входящие запросы и прочее) и check (setImmediate).

В веб-сервере под капотом Tramvai мы испольуем setImmediate для того что бы иметь возможность "разорвать" event loop между тяжелыми задачами по SSR и ответить на легкие запросы /metrics, /health и так далее.

У libuv есть логика по ограничению одновременно обрабатываемых immediate коллбэков - https://github.com/libuv/libuv/blob/49b6e4db0cfc2bdb4c4151030618981c2fc0795b/src/unix/core.c#L462-L465

http модуль внутри использует различные таймеры/интервалы, до обновления ноды все это вместе с request limiter работало так скажем в гармонии, логика по обработке запроса (в том числе таймеры) выполнялась на одной фазе event loop до immediate коллбэков.

После обновления libuv в Node.js, если я все правильно понял, логика обработки запроса теперь раскидана до и после check фазы с immediate коллбэками, и происходит что-то вроде взаимной блокировки - мы не можем полноценно обрабатывать новые запросы (в том числе быстро ответить 429 кодом) пока в очереди есть много immediate коллбэков.

Также Маттео накинул несколько кейсов почему в целом опасно использовать setImmediate для дробления обработки запросов и что это просаживает перф - https://github.com/fastify/fastify/pull/545

Проблем тут вижу несколько:
- в нашем кейсе, SSR это не тысячи а десятки RPS как в бенчмарках fastify/h3, и такие проблемы нам не важны
- но при этом у нас была реальная и очень полезная возможность оставаться отзывчивыми, и держать под нагрузкой адекватные 2xx RPS
- также я не вижу интеграционных тестов в репозитории under-pressure и сомневаюсь до конца что инструмент работает ожидаемо

Пару дней назад Маттео написал даже в блок Platformatic (это их коммерческий продукт для Node.js стека) про кейс, итого получился подробный обзор проблемы (правда черезчур нейросетевой):
- https://x.com/matteocollina/status/1951322487595090177?s=19
- https://blog.platformatic.dev/the-dangers-of-setimmediate

Основной поинт Маттео который я полностью поддерживаю - надо использовать worker_threads, и само приложение поднимать в воркере, таким образом изолировать его event loop (тут он рекламирует их сервер Watt который так делает из коробки)

Для Tramvai тут проблема что это сильно не вписывается в текущую архитектуру.

Придется делать еще один заход и смотреть как мы можем избавиться от setImmediate, и что в итоге выжать из Request Limiter под нагрузками на свежих версиях Node.js
  • 👍 16
  • 🔥 12
  • ❤ 7
Post #223 2.61K
Одна из классных идей в новой CLI - кастомные трейсы в формате Trace Event Format

Идея взята у Parcel, Rspack и Next.js, примеры:
- https://parceljs.org/features/profiling/#tracing
- https://github.com/parcel-bundler/parcel/blob/v2/packages/core/profiler/src/Tracer.js
- https://rspack.dev/contribute/development/tracing

Написал кастомный трейсер поверх либы chrome-trace-event, пример API:

const tracer = new Tracer();

tracer.wrap({ event: 'event' }, async () => {
await doSomethingAsync();
});


Во вложении пример визуализации кастомного трейса на сборку и несколько ребилдов, в интерфейсе https://ui.perfetto.dev/. Очень удобно смотреть сколько времени занимают основные операции, какие блокируют друг друга, где произошла ошибка (трейсы пишутся на диск не в конце а все время жизни скрипта)

В идеале - еще собирать более подробные трейсы по сборке через хуки бандлера.
  • 🔥 10
Post #222 1.76K
И раз уж зашел разговор о CLI, поделюсь одной из актуальных задач - разработка обновленной @tramvai/cli (уже писал про это короткий пост)

Во вложении - дизайн новой CLI, он уже претерпел ряд изменений, но основные концепции остались.

Какие основные цели для новой CLI:
- решить базовые проблемы с перформансом - основная, webpack MultiCompiler запускает все сборки в одном процессе, серверная и клиентская конкурируют между собой
- реализовать удобную систему плагинов (и первым же новым плагином интегрировать rspack)
- полностью разделить JS API и CLI API
- сделать общий набор тест-кейсов, который будет удобно запустить с разными плагинами - вебпак+бабель, вебпак+swc, rspack
- избавиться от легаси, улучшить отладку, упростить структуру

Итак, основная техническая задачка тут - ускорение двух параллельных вебпак сборок.

Тут очевидное решение - вынести их в worker_threads, что из коробки webpack и его MultiCompiler не умеет.

И главный челлендж тут - как передать конфигурацию из CLI в воркеры, если там могут быть плагины - то есть не сериализуемые методы/классы/прочие объекты?

Этот кейс решил следующим образом:
- есть общая логика - чтение tramvai.config.ts конфигурационного файла, где могут быть плагины
- есть набор входящих сериализуемых параметров, которые можно передать через CLI (`tramvai start ...`) или JS API (`new Tramvai().start(...)`)
- и основной процесс и webpack воркер - считывают один и тот же конфигурационный файл
- входящие параметры пробрасываются при старте воркера из основного процесса

Вокруг воркеров сделал небольшие удобные обертки для контроля и коммуникации.

Основной пакет @tramvai/api определяет базовые интерфейсы - DevServer и Builder, ждет их в DI контейнере, и запускает их жизненный цикл.

Вся логика с webpack, реализация DevServer на основе webpack-dev-middleware и соответствующие зависимости - в отдельном @tramvai/cli-plugin-webpack плагине, аналогичный будет для rspack.

Все babel зависимости и фабрика babel конфига - в отдельном @tramvai/cli-plugin-babel, и соответственно такой же будет для swc.

Набор тестов сделан как фабрика, под разные плагины но с общими кейсами и фикстурами.
Стараюсь добавлять тест-кейс на каждый значимый параметр сборки, который мы явно конфигурируем.

Похоже основным челленджем далее будет - миграция пользователей и временная поддержка двух реализация команды tramvai start.
  • 🔥 6
  • 👍 3
Older posts →

About this channel

How can I read @super_oleg_dev without a Telegram account?
TGViewer shows the public web preview Telegram publishes for SuperOleg dev notes: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does SuperOleg dev notes have?
SuperOleg dev notes (@super_oleg_dev) has 1.75K subscribers on Telegram, refreshed roughly every 30 minutes.
Does SuperOleg dev notes 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 →