SuperOleg dev notes
1.87K subscribers
56 photos
149 links
Обзоры новостей и статей из мира frontend, интересные кейсы, исследования и мысли вслух

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Download Telegram
И сразу пример работы preload тега, в Chrome Devtools никак не увидеть, что TCP + TLS происходит раньше
Второе - это особенность HTTP/2 по отношению к поддоменам.

Я не уверен что это общая особенность, скорее наша специфика.

Допустим, у нас на странице есть несколько поддоменов:
- www.cdn-tinkoff.ru
- imgproxy.cdn-tinkoff.ru
- unic-cdn-prod.cdn-tinkoff.ru

Они резолвятся в один и тот же IP нашего CDN провайдера.

С них загружается JS и шрифты с crossorigin="anonymous" и CSS с изображениями без CORS, это дефолтное поведение любого запроса за ресурсами.

И для запросов с CORS (это наши JS и шрифты) по HTTP/2 браузер переиспользует один и тот же сокет, и тут не большая проблема во множестве поддоменов.

А вот для наших CSS и изображений это уже не работает, запросы без CORS и переиспользуется только резолв DNS, но не установка TCP+TLS соединения.

И в итоге можно наблюдать вот такую картину, и поначалу сложно разобраться по какой логике переиспользуются соединения
Привет!

Хочется подвести какой-то полезный итог всей истории по preconnect и по загрузке ресурсов в целом.

Лучшее что мы можем сделать - это вообще не использовать CDN.
А точнее, сделать так что бы CDN был на домене нашего приложения - например такую инфраструктуру предлагает Vercel.
Отказываться от преимуществ CDN не хочется и не нужно)
Тогда все основные ресурсы приложения можно раздавать с того же доменного имени, при использовании HTTP/2 будет создан только один TCP сокет при запросе самого HTML документа, и никаких дополнительных накладных расходов на установку соединения в тот момент, когда браузер перейдет к скачиванию CSS и прочих ресурсов.

Это большие изменения инфраструктуры, и компромиссом будет иметь максимум один домен для CDN, но тут мы можем столкнуться с проблемой разных значений атрибута crossorign - и все-равно получим лишнее TCP соединение, установка которого может отложить загрузку наших критичных скриптов или например LCP изображения.

crossorign="anonymous" - действительно важный атрибут для скриптов со сторонних доменов, если мы хотим качественный мониторинг ошибок.
И раз мы вынуждены использовать его для скриптов и шрифтов, кажется что лучше всего так же по умолчанию добавлять его и для стилей и для изображений.
Также, не забывать и про preload теги, если атрибут crossorign не совпадает, то предзагруженный контент не будет переиспользован.

Но допустим мы не контролируем в приложении отрисовку изображений или добавление части ресурсов.
Для решения этой проблемы может помочь <link rel="preconnect" href="https://example.com" /> для CDN, которому также будет важен crossorign - это зависит от вашего водопада критичных запросов за ресурсами, какой preconnect стоит использовать.

Еще больше ускорить загрузку помогут Early Hints - если вы рендерите приложение на сервере, и делаете запросы в сторонние API, блокирующие рендер, отдать все необходимые preconnect и preload теги в самом начале обработки запроса - в теории бесценно.
На практике, есть нюансы, и для нас пока главный инфраструктурный - 103 код ответа не уходит из нашего k8s кластера (research in progress).
Хотя по документации Envoy балансер (используется в k8s) поддерживает 103 код, хоть и с ограничением - отдает только первый, но по спецификации можно отдать сколько угодно 103 ответов до 200 финального.
В другой инфраструктуре балансер вообще не смог распарсить и проксировать Early Hints и не отдавал страничку, хорошо что мы с нее переезжаем и не надо копать в эту сторону.
👍3🔥1
Следующее, это касается и загрузки страницы, и Early Hints - это нюансы работы сети. Даже с HTTP/2 и мультиплексированием запросов, канал пользователя ограничен, и чем больше ресурсов мы запрашиваем, тем сильнее они конкурируют между собой.

Поэтому не стоит загружать сразу все, делать preconnect к десятку доменов, preload всех скриптов и стилей одновременно, надо выбирать приоритеты.
Для разных приложений могут быть совсем специфичные приоритеты, но допустим у нас обычный SSR и советы тоже будут типовые.

Самое важное для отрисовки - это CSS (блокирующий ресурс) и шрифты. Есть много гайдов по оптимизации, тут лишь коротко отмечу несколько полезных вещей:
- инлайн небольших стилей в HTML разметку
- теоретически critical CSS (ни разу не встречал серьезных кейсов использования)
- не использовать веб шрифты или хотя бы выбрать оптимальный font-display
- для предотвращения CLS использовать новые крутые свойства size-adjust и прочие для того что бы fallback шрифт был максимально похож размером на нужный вам - https://screenspan.net/fallback

Следующее - это LCP изображение (если оно изображение, а не текст).
Стоит предусмотреть механизм, который будет автоматически добавлять preload тег с нужным изображением на страницу.
При использовании srcset - обратите внимание на нюансы из этого гайда - https://www.stefanjudis.com/today-i-learned/how-to-preload-responsive-images-with-imagesizes-and-imagesrcset/, самый важный - нельзя добавлять src атрибут для прелоад тега, иначе будет двойная загрузка, кажется в Safari.

Когда наши CSS файлы загружены, хотя бы один из них - можно начинать грузить JS. Такое можно провернуть например через onLoad обработчик для link тега со стилями, tramvai добавляет preload теги для скриптов таким образом.
Но даже если мы хотим пораньше получить интерактивное приложение, при использовании SSR пользователь уже получить какой-то полезный контент, и не стоит блокировать рендеринг приложения нашим джаваскриптом - и всегда использовать атрибут defer.
Да, что-то может сильно задержать событие DOM interactive и соответственно выполнение скриптов, но атрибут async мне не нравится, так как скрипт будет выполнен по завершении не блокирующей загрузки, а выполнение JS это блокирующая операция, и могут быть конфликты к примеру с загрузкой стилей, или доступом к еще не обработанным DOM элементам.

Различную аналитику надо стараться загружать и выполнять настолько поздно, насколько это возможно.
Также очень круто выглядит Partytown, умеющий выполнять скрипты в воркере с полным доступом к DOM.

Последнее что вспомнил - это водопад загрузки скриптов и стилей для конкретной страницы.
Если вы заботитесь о пользователях и используете разделение кода по роутам и тяжелым компонентам, важно загружать ресурсы нужные именно для этой страницы параллельно.
Мета-фреймворки решают эту задачу из коробки, и при отрисовке страницы на сервере, и при SPA-переходах.
Но классические SPA-приложения, исключительно с клиентским рендерингом, всегда будут страдать от этой проблемы, так как на одной общей index.html точке входа в приложение мы не можем сразу добавить ресурсы для всех роутов приложения.
SSG заглушек для каждой страницы будет более предпочтительным вариантом, хоть у него и есть свои ограничения.

Расскажите про оптимизации флоу загрузки ресурсов на ваших приложениях, какие-то интересные подходы и лайфхаки, детали о которых я забыл рассказать?
👍11🔥31
Привет!

У Angular ну очень классное API для HTTP Interceptors:

```
(request, next) => next(request)
```

Где next(request) возвращает response.

И для сравнения API у axios - https://axios-http.com/docs/interceptors

Это закрывает просто все необходимые кейсы:
- изменить параметры запроса
- вернуть кастомный ответ или ошибку
- модифицировать ответ или ошибку

Забираю в tramvai и депрекейчу накиданные мной на коленке страшные modifyResponse, modifyRequest и modifyError коллбэки для встроенного HTTP клиента)

Правда Angular создает инстанс Observable при вызове next, в tramvai Rx не используется, и будут обычные промисы:


[
// меняем параметры запроса
(req, next) => next({ ...req, timeout: 1000 }),

// модифицируем ответ
(req, next) => next(req)
.then((res) => ({ ...res, payload: 'Intercepted' }),

// модифицируем ошибку
(req, next) => next(req)
.catch((reason) => {
Object.assign(reason, { code: 'INTERCEPTED_ERROR' });
throw reason;
},
]

Мелкая фича, но всегда очень приятно встретить и позаимствовать лаконичное и функциональное API
🔥12👍3
И еще немного про Angular и вообще - коллеги выпустили универсальную либу для маскирования полей ввода, плюс биндинги для Angular (React в ближайших планах) - Maskito

Было очень интересно почитать насколько много нюансов и проблем с нативными событиями полей ввода при реализации этой либы - https://habr.com/ru/companies/tinkoff/articles/727368/

Не знал про событие beforeinput, выглядит очень хорошо, особенно нравится event.inputType

Будет очень круто, если наконец-то появилась та самая маска, которая решит большую часть кейсов, с которыми наверное многое из нас успели пострадать)
👍10
Привет!

Закончил очередной этап переработки документации tramvai.dev

Постарался значительно улучшить доку по нашему решению для микрофронтов - Child Apps

Теперь основные возможности Child Apps вынесены на передний план, отдельно описана интеграция с хостовым приложением, отдельно как это все работает вместе.

Пользуясь случаем, порекламирую решение, и коротко расскажу почему оно крутое:
- Это SSR микрофронты, полноценная композиция на стороне сервера в виде обычных React компонентов в дереве приложения
- Это фреймворк, у микрофронта есть свой жизненный цикл, механизмы для загрузки данных, работы с роутингом, добавлению ресурсов на страницу, механизм Dependency Injection интегрированный с DI хоста
- Это CLI, решены все вопросы сборки микрофронта
- DX при разработки Child App очень похож на DX при разработке обычного tramvai приложения - буквально во многих местах используются одни и те же модули и провайдеры
- Интеграция React Query, та же самая что и для tramvai приложения
- Интеграция Module Federation (ох непростой кейс для SSR), можно шарить все основные tramvai библиотеки, тот же React Query, базовый вес микрофронта получается очень небольшой

То есть, Child App это не про оркестрацию микрофронтов написанных на пяти разных фреймворках и запущенных на одной странице.

Это возможность создать мини-tramvai приложение, независимый блок с UI на React и своей логикой, с отдельным релизным циклом и возможностью переиспользовать его во множестве приложений.

Добавил Child App в шаблон на основе tramvai приложения на Codesandbox, что бы было легко попробовать - https://codesandbox.io/p/sandbox/romantic-sun-1r6rsg?file=%2Ftramvai.json

Для сборки микрофронта нужно запустить отдельный терминал и команду yarn child:start, а tramvai приложение должно запуститься самостоятельно на 3000 порту
👍10🔥91
Ожидаемый результат в песочнице
👍5
Привет!

Занимаюсь задачкой по различным интеграциям для удобной разработки PWA приложения на tramvai, хочется написать некий лог разработки, прокрутить все еще разок в голове, поток мыслей в общем.

У нас уже был внутренний модуль, который просто подключал со стороны Service Worker и webmanifest, а сам SW и манифест разрабатывались и загружались на s3 отдельно от приложения.

Но сейчас под планируемый функционал у нескольких приложений хочется получить DX близкий к разработке приложения, возможность более быстрой итерации от изменения кода до получения результата.

По фичам PWA:

- Нам нужно генерировать сервис-воркер
- Также генерировать webmanifest
- Дополнительно генерировать иконки
- Интегрировать все это в приложение

По технологиям:

- workbox помогает легко реализовать все популярные кейсы с SW (кэширование, оффлайн) + подробные логи
- workbox-window умеет правильно подключать SW и упростить общение с ним из приложения + подробные логи
- sharp как популярное решение для работы с изображениями

В идеале сделать workbox опциональным и дать возможность генерировать SW без его участия, но для начала можно зашить его в конфигурацию.

Есть ряд PWA плагинов под популярные мета-фреймворки, в качестве референса использую плагин для Nuxt.js - https://pwa.nuxtjs.org/

Почему именно ее:

- Nuxt.js один из немногих мета-фреймворков с адекватной расширяемой архитектурой, полноценно поддерживает плагины, в которых можно конфигурировать build-тайм и рантайм фичи
- Отличная декомпозиция - выделены модули по фичам: Workbox, Manifest, Icon и Meta - готовая ментальная модель для разработки похожего функционала
- Подробная документация, описаны конфигурации для каждого модуля, можно переиспользовать

Кстати этот плагин как раз поддерживает работу без workbox.
👍10
Один из главных челленджей в задачке - поддержка фич PWA в development режиме.
Сгенерировать SW, вебманифест и иконки на этапе сборки приложения или сразу после, значительно проще.

Один из плюсов workbox - готовые webpack плагины - для генерации SW с нуля или для внедрения манифеста сборки в существующий исходник сервис-воркера. Манифест необходим если мы хотим предварительное кэширование основных ассетов приложения.

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

С этим workbox плагином есть проблема - он не поддерживает работу в watch режиме, что оказалось легко переопределить, но осталась нерешенная проблема с Hot Module Replacement - после пары апдейтов на клиенте HMR зацикливается и я так и не добрался до причины)

Итак, добавляю поддержку исходного файла с сервис-воркером написанном на TS, на основе которого плагин InjectManifest будет генерировать sw.js с добавленным манифестом сборки.

Тут всплыл отдельный кейс с нашей modern сборкой (те же чанки с кодом в более современном формате, имеют другие хэши), простым решением оказалось генерировать sw.js и sw.modern.js, в каждом из которых свои манифесты сборки с подходящими чанками.
👍5
Дальше нужно генерировать manifest.json и иконки. Я решил интегрировать это в процесс сборки, причин несколько, не уверен что перевешивают сложность реализации:

- Доступ к манифесту и иконкам через наш dev server, не надо писать их на диск во время разработки
- Хочется отслеживать изменения исходного файла с иконками
- Возможно понадобится иметь их в манифесте сборки

Кстати до сих пор не знаю, нужно ли предварительное кэширование для вебманифеста и иконок)

В итоге, пришлось сделать два небольших вебпак плагина, которые умеют их генерировать. Отсутствие хорошей документации это отдельная боль, хорошо есть тайпинги и много примеров плагинов в node_modules в качестве референса, пригодилось даже для таких простых кастомных плагинов.

У sharp простое API, но надо продумать такие кейсы как ресайз картинок только если исходник реально изменился + персистентный кэш между сборками для ускорения процесса.

Отлично, у нас есть сервис-воркер, вебманифест и иконки, пора интегрировать в приложение.

Сделал общий новый PWA модуль с декомпозицей по вложенным модулям 1 в 1 как у Nuxt.js плагина.

Для сервис-воркера - на клиенте регистрируем его через workbox-window (вынес либу в динамический чанк, уменьшает надежность но не хочется тянуть лишние килобайты кода на клиент).

При кэшировании бандлов приложения, они не кэшируются намертво, что заблокировало бы разработку - InjectManifest создает манифест сборки с актуальными ревизиями файлов в development режиме, это меняет содержимое воркера при пересборке, браузер после загрузки видит что SW изменился и грузит новый (принудительный update воркера позволит сделать это быстрее) - итого + одна перезагрузка страницы, но мы получаем в итоге актуальный код в браузере, кажется нормальный компромисс для разработки PWA.
Также сделал опцию на отключение фичи в dev режиме.

Для вебманифеста - просто добавляем его в head приложения, как и мета теги.

Интеграция иконок заканчивается на добавлении их в вебманифест.
👍2
Также, локально нам нужен доступ на относительный урл /sw.js или какой-там урл мы сконфигурируем с желаемым скоупом. В tramvai приложении это прощее всего сделать добавив конфиг для http-proxy-middleware, который будет смотреть на dev-сервер.

Спустя десяток-два новых тудушек в репе, базовая интеграция готова, интересно какие кейсы всплывут на приложениях коллег.

Хочется выявить и добавить какие-то общие рецепты/паттерны, может даже в виде кода для импорта в сервис-воркере, с фичами реализованными на workbox - например при генерации статических страниц (SSG) - кэшировать просмотренные для работы оффлайн. Сгенерированные в рантайме кэшировать все-таки не безопасно.
Также у нас есть механизм для работы в режиме CSR с одной страницей-заглушкой - вот ее точно можно кэшировать.

Важный инсайт при разработке получил по поводу текущей архитектуры трамвая.

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

Но билдтайм, это параметры @tramvai/cli, у нас не расширяемый, пользователь не может поменять вебпак конфиг или вклиниться на другие этапы сборки. И по сути разрабатывая независимый функционал, часть которая касается сборки мне приходится зашивать в код cli, усложнять его и в целом риск поломки есть всегда.

Хороший повод поскорее реализовать систему плагинов-модулей для cli - у нас уже многое для этого готово, добавляли в cli наш dependency injection контейнер для абстракции над сущностью Builder - подготовка к возможности в будущем поддерживать разные бандлеры.

Второй инсайт, это то что у нас разделен рантайм и билдтайм.

К примеру у Nuxt.js плагины имеют доступ одновременно к параметрам сборки, и могут менять что-то в рантайме.

В моем случае, из-за того что функционал сильно связан между собой, приходится делать ряд манипуляций - передавать какие-то параметры из cli в клиентский код PWA модуля, думать куда ложить конфигурацию под разные фичи - в статичный конфиг файл tramvai.json или в рантайм через DI провайдеры (к этом у cli нет доступа).

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

А что для вас было бы идеальной интеграцией возможностей PWA в приложении?
👍4
Пример классных логов от workbox и workbox-window при разработке, регистрация сервис-воркера
Обновление сервис-воркера. HMR кстати иногда работает))
Баг дня - в Safari не работает проставление cookies без значения если есть дополнительные параметры, пример - test; SameSite=None; Secure

И дело тут на удивление не в SameSite - как я понимаю Safari более строго следует спеке, и записать надо либо с value, или как минимум добавить знак равенства:


test=enabled; SameSite=None; Secure`
test=; SameSite=None; Secure`

При том, что document.cookie = 'test' сработает корректно.

Не нашел похожих issue или тредов по проблеме, присылайте ссылочки есть уже сталкивались с такой проблемой
👍6
Привет!

Одна из первый идей, которую я реализовывал в Тинькофф - ленивая гидрация для React компонентов.

Ленивая гидрация позволяет не выполнять реакту код компонента на клиенте сразу при гидрации всего приложения, и отлично комбинируется с IntersectionObserver - можно выполнять код только при попадании компонента в область видимости.

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

На tinkoff.ru очень тяжелый футер, и lazy hydration просто отлично зашла для его оптимизации.

Механизм достаточно простой, это по сути легализованный хак в React, детали есть в этом issue - https://github.com/facebook/react/issues/10923#issuecomment-338715787

Код для lazy обертки может базово выглядеть так:

```
const LazyRender = ({ children }) => {
const containerRef = useRef(null);
const isVisible = useObserver(containerRef);

if (isVisible) {
return React.createElement('div', {}, children);
}

return React.createElement('div', {
ref: containerRef,
suppressHydrationWarning: true,
dangerouslySetInnerHTML: { __html: '' },
});
};
```

Абстрактный хук `useObserver` должен вернуть `true` всегда на сервере, либо при попадании компонента в область видимости на клиенте - то есть мы сознательно создаем ошибку гидрации, и гасим ее через пропс `suppressHydrationWarning: true`.

Вся магия, почему разметка на клиенте не ломается, а переиспользуется, как раз заключается в хаке с `dangerouslySetInnerHTML: { __html: '' }`.

У решения конечно же есть минусы:
- приходится создавать лишний тег - враппер
- по умолчанию код все-равно остается в клиентском бандле
👍21
Теперь хочется обсудить современные возможности React, и как они могут помочь ускорить гидрацию и уменьшить количество клиентского кода, и нужен ли еще этот хак.

В первую очередь это конечно React Server Components - подход Next.js с серверными компонентами по умолчанию в теории может кардинально уменьшить количество клиентского кода.

А стриминг разметки и данных с сервера на клиент вместе с механизмом selective hydration делает процесс гидрации неблокирующим и постепенным, также это все идет в паре с вложенным роутингом и гранулярным разделением кода на чанки.

Концепция RSC очень крутая, и это полноценный конкурент таким подходам как у Resumability у Qwik или Islands Architecture.

На практике у RSC + Next.js не все идеально, есть вопросы и к производительности на сервере, и к DX и сложным концепциям, вендор лок на некст, но тем не менее RSC это скорее всего грядущее будущее экосистемы реакта, в том или ином виде.

Окей, у нас не Next.js, что есть еще интересного и более доступного?

Это как раз механизм selective hydration - неблокирующей гидрации, почитать про это и вообще целевую SSR архитектуру можно тут - https://github.com/reactwg/react-18/discussions/37

Даже без RSC, в React 18 есть возможность стримить разметку с сервера, и гидрировать ее на клиенте постепенно, небольшими неблокирующими задачами - то есть напрямую улучшить время до интерактивности важного контента на странице.

Про стриминг React приложения на сервере и его проблемы уже писал ранее - https://t.me/super_oleg_dev/49

По умолчанию, Suspense + стриминг + selective гидрация не решают проблему больших клиентских бандлов - но это уже задача `@loadable` или `React.lazy` для выделения кода компонентов в отдельные чанки с помощью динамического импорта.

И в целом, большая часть проблем решается - ваши тяжелые компоненты вынесены в lazy чанки, обернуты в Suspense, приложение гидрируется через hydrateRoot, время до интерактивности улучшилось, а кода стало меньше.

Но к сожалению тут есть и минусы:
- с SSR, мы автоматически предзагружаем скрипты lazy чанков которые были отрендерены на странице на сервере. Это расходы на сеть, и возможно на компиляцию кода, даже если он выполняется не сразу
- если отдельные блоки не обернуты в Suspense, при действиях пользователя, например клике по кнопке в процессе гидрации - React запустит принудительную блокирующую гидрацию у всего поддерева компонентов до ближайших Suspense границ
- ошибки гидрации не редкость, они также приводят к принудительной блокирующей гидрации или даже рендеру

В итоге, кажется подход с lazy гидрацией остается актуальным, и позволяет делать настоящие независимые Islands - отдельные островки из которых может состоять приложение.
🔥17
Почему я вообще поднял тему lazy hydration.

Сейчас исследую возможность оптимизировать тяжелый футер tinkoff.ru не только на клиенте, но и на сервере.

В теории, его можно кэшировать, и не рендерить для каждой страницы заново - а время его рендеринга занимает 20-30% от общего времени рендера большинства страниц, то есть оптимизация напрямую повлияет на максимальные нагрузки, которые могут обрабатывать приложения.

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

Если искать информацию по кэшированию React компонентов, можно найти несколько устаревших форков рендерера `ReactDOMServer`, например https://github.com/rookLab/react-component-caching

Этот подход сложный в реализации и поддержке, и не удивительно что актуальных решений кажется не существует.

Но оказалось что можно сделать очень простой локальный механизм, который отлично ложится на наш `LazyRender` компонент, настолько простой что до сих пор ищу подвох.

Сразу покажу обновленный код:

```
const LazyRender = ({ children, cacheEnabled, cacheKey, serverCache }) => {
const containerRef = useRef(null);
const isVisible = useObserver(containerRef);

if (cacheEnabled && typeof window === 'undefined') {

let html: string;

if (serverCache.has(cacheKey)) {
html = serverCache.get(cacheKey);
} else {
const reactDomServer = require('react-dom/server');

html = reactDomServer.renderToString(children);

serverCache.set(cacheKey, html);
}

return React.createElement('div', {
dangerouslySetInnerHTML: { __html: html },
});
}

if (isVisible) {

return React.createElement('div', {}, children);
}


return React.createElement('div', {
ref: containerRef,

suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: '' },
});
};

```

Где `cacheKey` - уникальный ключ для кэширования конкретной вариации children компонента, он должен иметь низкую кардинальность для эффективной работы кэша.

А `serverCache` - любой LRU кэш со стандартными методами has / get / set, и настроенным не слишком высоким временем жизни кэша.

Для этого кэша на сервере важно быть синглтоном, в принципе как и для любого другого.

В `LazyRender`, если кэш включен, мы вручную рендерим children в HTML строку, и дальше сохраняем результат в кэши и используем при рендере других страниц.

Основной минус подхода - React Context приложения будет недоступен в children компоненте. Мы планируем использовать это для микрофронта (а футер как раз такой микрофронт), где это кажется не проблема, и даже хорошая практика.

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

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

Также, добавил серверную метрику - счетчик с лейблами hit / miss и уникальным ключем кэша, для возможности чекать hit rate и в целом эффективность кэширования.

Обязательно поделюсь результатами, как дело дойдет до интеграции и подключения на продакшене!
🔥17👍4
Привет!

Сделали очередной заход на добавление кэша DNS lookup.

Теория - резолв DNS синхронный, и треды libuv занимает, и негативно влияет на производительность приложений, лишняя нагрузка на event loop.

Использовал либу https://github.com/szmarczak/cacheable-lookup

TTL кэша высокий ставить кажется опасным, начали с 15 секунд, оставил 1 минуту, и хочется попробовать еще повысить - риск тут вижу, что можем ловить совершенно не нужные ошибки запросов к сторонним сервисам в момент смены этими сервисами IP адреса, что по идее может происходить не редко в k8s кластерах.

На этой неделе раскатили и понаблюдали на одном приложении, какой эффект получили по HTTP запросам:
- заметно ускорились запросы, которые идут в обход трамвайных HTTP клиентов
- незначительно ускорились запросы через наши базовые HTTP клиенты - тут большой буст к перформансу уже дает активная опция keepAlive для http/https агентов ноды, установка соединений происходит гораздо реже. Плюс lru-кэши и дедупликация для запросов.
- соответственно на наших метриках активных соединений почти пропали GetAddrInfoReqWrap

По производительности, сначала показалось что эффекта вообще нет - приложение рестартует, отъедает побольше памяти, и снова повышается лаг эвент лупа - кажется по большей части из-за роста времени работы Garbage Collector.

Но к концу недели все-таки стал заметен эффект, пиковые значения лага эвент лупа, времени сбора мусора, потребление CPU и CPU троттлинг - все стало пониже, пример графиков закину в канал отдельным сообщением (там где перцентиль не указан вроде бы 95).

Я думаю, этот эффект был бы менее заметен если бы мы выделяли подам больше 1000 mCPU в k8s, так как и треды libuv и сборка мусора выполнялись бы по настоящему параллельно, и не было бы такого высокого CPU троттлинга.

Хорошая статья, где предлагается оптимальным использовать 1150 mCPU, или 1.15 ядра на под, и объясняются все механизмы из предыдущего абзаца - https://medium.com/pipedrive-engineering/how-we-choked-our-kubernetes-nodejs-services-932acc8cc2be

По итогу, кажется хорошая и почти бесплатная оптимизация, посмотрим, выдержит ли испытание временем.

Обязательно делитесь вашим опытом аналогичных оптимизаций, и в особенности не удачных!
🔥42👍2