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

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Download Telegram
В заголовке статьи присутствует совершенно новый для меня термин - Resumable Javascript. Его подробное объяснение можно прочесть в репозитории фреймворка на github - https://github.com/BuilderIO/qwik/blob/main/docs/RESUMABLE.md, и в этом комментарии - https://dev.to/ryansolid/comment/1lnip к статье про проблемы эффективной гидрации.

Для понимания термина, приводится пример современных фреймворков, которые являются Replayable - после инициализации приложения на сервере, требуется полный replay приложения на клиенте, т.е. процесс гидрации со всеми его минусами.

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

Qwik делает это возможным благодаря сериализации всех важных данных в HTML разметке, и разделения их на категории:

- Указатели на DOM события
- Состояние компонентов
- Указатели на шаблоны компонентов
- Связи между различными реактивными состояниями

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

Итоговая цель фреймворка - точечная регидрация компонентов вне зависимости от их положения в дереве компонентов приложения. Summary по ленивой загрузке также доступно в репозитории фреймворка - https://github.com/BuilderIO/qwik/blob/main/docs/LAZY_LOADING.md.

Между делом, попался интересный issue - https://github.com/solidjs/solid/issues/264 по поводу Partial Hydration для Solid.js, где автор рассматривает ряд вариантов для реализации фичи, и самой перспективной видит возможность грузить template код компонентов по требованию.

Более подробная статья про то, как Qwik добивается гранулированной загрузки данных - https://dev.to/builderio/qwik-the-answer-to-optimal-fine-grained-lazy-loading-2hdp

Демка todo-app на Qwik - https://stackblitz.com/edit/qwik-todo-demo. В этой демке очень наглядно, при взаимодействии с приложением, загружается по отдельности JS код обработчиков и шаблонов компонентов.

В итоге, не разбирая специально статью Resumable JavaScript with Qwik, мы почти полностью рассмотрели все концепции, которые она затрагивает. Рекомендую к прочтению и комментарии к статьям, в них есть много интересных мыслей.

Например, из интересного, есть twitter тред про Partial Hydration - https://twitter.com/RyanCarniato/status/1489077057942220803, где один из авторов Remix Райан Флоренс говорит о том, что демки не доказывают профита от всех этих сложностей с гидрацией, и в Remix отлично работает подход Progressive Enhancement.

Мне показалось это не очень честным, например можно увидеть значительно большее количество кода у Remix (читай React) демки, чем у Marko и Qwik. Также, сложно назвать это demo приложение чем-то сложным и большим, для настоящих приложений с тяжелыми страницами частичная гидрация принесет еще больше заметной пользы.

Единственное, что мы пока не рассмотрели, это проблемы фреймворка и подхода частичной гидрации. Если коротко:

- Qwik еще сырой
- Документация не полная
- Даже при частичной гидрации есть критичные вещи, которые надо грузить сразу
- Потенциальный waterfall загрузки скриптов, лучше грузить все параллельно или в одном бандле
- Необходимо умное разделение кода, это требует большой работы над тулингом

Думаю это не полный список, и определенно стоит ждать real-world кейсов использования фреймворка.
В качестве итога, хочу поделиться вдохновением, которое вызывает развитие современных концепций и инструментов, и всего прочего, что касается рендеринга web приложений. Мы еще очень далеки от идеала, многие подходы совершенно разные, и пока все становится только лучше и интереснее!

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

Как мне кажется, это проблемная область, т.к. SSR приложения держат очень маленькие нагрузки, из-за того что рендеринг сильно нагружает капризные NodeJS сервера тяжелой синхронной работой.

Например, рендеринг средней страницы в React приложении занимает около 100мс, в таком случае приложение вряд ли сможет обрабатывать больше 10-15 RPS, что совершенно не high load. Это актуально для всех SSR приложений, где нет возможности кэшировать результат рендеринга по разным причинам - A/B тесты, персонализация, микрофронтенды.

Надеюсь в будущем и для серверного перформанса мы увидим прорывы и новые подходы, улучшающие эту ситуацию.
👍4
Привет!

Вдогонку к предыдущему посту, не могу не поделиться ссылкой на разбор всех возможных видов рендеринга в web от Эдди Османи.
Очень хорошее summary.

https://www.patterns.dev/posts/rendering-patterns/
👍6
Привет!

Активно готовим tramvai к React 18, хочу поделиться важными для меня плюсами, и к сожалению минусами.

Рендеринг на сервере стал быстрее.
На одном простом демо приложении, RPS вырос с 50 до 70 просто при обновлении react и react-dom.
Т.к. именно рендеринг компонентов это главный ограничитель RPS любого SSR приложения, это очень приятный сюрприз, и самое главное, бесплатный.

Улучшается метрика Total Blocking Time, если просто обернуть приложение в Suspense где-нибудь на верхнем уровне.
Вместо одной синхронной задачи по гидрации, React начинает гидрировать приложения маленькими кусочками.
Во вложенных скриншотах, с Suspense, TBT с 2s падает меньше чем 1s.
Улучшение TBT повлияет на Web Vitals метрику First Input Delay, которая имеет большой вес в общей оценке производительности.

Новые API startTransition и useTransition могут очень сильно улучшить рантайм перформанс.
Подходящие кейсы для использования - практически все, что связано с пользовательским вводом.
Наглядная демка от моего коллеги - https://stackblitz.com/edit/react-n4qzgr

Из грустного, светлое будущее потокового рендеринга HTML пока не наступило)

Кажется в целом, HTML стриминг переоценен, никто серьезно не использует его в продакшене, никто точно не знает, как он влияет на SEO.
Нет возможности сделать серверный редирект, после отдачи первого байта на клиент.
Также, браузеры ведут себя по разному при получении HTML потока, при первой загрузке и при MPA переходах между страницами:
- Safari ведет себя непредсказуемо, независимо от типа рендеринга, может моргать белый экран
- Firefox - нет белого экрана при стриминге, ждет полную загрузку страницы
- Chrome - белый экран, если стрим отвечает дольше 5и секунд, либо если в стриме отдан тег body, и затем начинается потоковый рендер новой страницы

Новые API React 18 для потокового рендеринга работают мягко говоря не производительно, больше инфы тут - https://github.com/facebook/react/issues/24232.
Поэтому трамвай будет продолжать использовать renderToString.
Это означает, что хоть Suspense и можно теперь рендерить на сервере, но его возможности ограничены.
Например, использование React.lazy может вызывать ошибку на клиенте - https://github.com/facebook/react/issues/24125
Поэтому мы рекомендуем продолжать использовать loadable для разделения кода.
👍11
Отдельно исследовал, как дела у Next.js, хотел убедиться что проблема не в моей реализации HTML стриминга:

- потоковый рендеринг (который у Next.js в альфе) работает очень медленно, скорее всего по больше части из-за медленного renderToPipeableStream

- в обычном режиме, фича Font Optimization очень сильно замедляет скорость рендеринга страницы - https://github.com/vercel/next.js/issues/35797 - и эта фича включена по умолчанию
Скриншоты гидрации без Suspense, и с обернутым в Suspense приложением
Привет!

Наконец-то добрался рассказать про релиз Nuxt 3 Release Candidate.

Кстати, апрель был полон новостей про мета-фреймворки: была презентация мета-фреймворка RedwoodJS и интерактивный доклад от Рича Харриса про будущее SvelteKit.

Мне очень интересно, как развиваются Redwood и SvelteKit, но рассказывать всегда стараюсь про что-то, что меня вдохновило.
У Redwood крутые интеграции с GraphQL и Storybook, SvelteKit показывает отличный DX с Vite, Рич предлагает ряд интересных идей.
Но все-равно, в обоих фреймворках очень много спорных концепций, особенно по поводу file-system конвенций именования файлов, и совершенно никаких механизмов для разработки больших и сложных приложений.
Пример таких механизмов, позволяющих разделять приложения на строительные блоки, и разделять ответственность - Dependency Injection и модули, плагины, да даже миддлвары (никогда больше!).

А вот релиз Nuxt очень меня вдохновил, по двум причинам: система модулей и целая экосистема для создания инструментов, унифицированных под разные бандлеры.

Начнем с модулей, но немного издалека.

Фреймворк tramvai дает пользователям возможность создавать модули, благодаря использованию DI. Пример таких модулей, которые отвечают за независимый друг от друга функционал - seo, http-client, cookie, metrics.
Решили делать интеграцию с react-query - создали обертку и модуль, подключение одной строкой кода которого дает пользователю все необходимые возможности для использования.
И так добавляется практически любой функционал.

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

Кстати, очень лаконичный пример модуля Nuxt, добавляющего css link на страницу:
export default defineNuxtModule({
setup (options, nuxt) {
nuxt.options.css.push('font-awesome/css/font-awesome.css')
}
})


tramvai модуль будет более многословным из-за синтаксиса провайдеров и заранее заданной гибкости (можно добавить от инлайн скрипта до мета-тега, и расположить их в один из слотов от начала head до конца body):
@Module({
providers: [{
provide: RENDER_SLOTS,
multi: true,
useValue: {
slot: ResourceSlot.HEAD_CORE_STYLES,
type: ResourceType.style,
payload: 'font-awesome/css/font-awesome.css',
}
}]
})
class FontAwesdomeModule {}


В других фреймворках, возможности намного более примитивны.
Remix предлагает экспортировать свойство links, в Next вообще возможен только прямой импорт файла, а для мета-тегов предлагается решение вида все-в-React.

Идем дальше!
8👎1
Самое крутое, что было в анонсе - это экосистема Unified JavaScript Tools.
Часть этих инструментов предназначена для абстрагирования от конкретных бандлеров, часть - для универсального использования в браузере и на сервере (NodeJS, Deno, Workers).

Хотите переходить с Webpack на Vite (возможно спешить не стоит, Vite у Storybook не сильно обогнал Webpack 5 + lazyCompilation) и переживаете, что делать с вашими любимыми кастомными плагинами?
Вот вам Unplugin - абстракция, позволяющая писать один плагин, который будет работать с webpack, rollup, vite и esbuild.

Unbuild - бандлер поверх Rollup для сборки универсальных пакетов.

Nitro - сервер с пачкой возможностей для прокачки вашего SSR, c поддержкой file-system роутов и API.

Consola - универсальный логгер.

Unstorage - абстракция с драйверами под все виды хранилищ на сервере и клиенте.

ipx - image proxy, которую вы можете запустить хоть на эндпоинте вашего приложения (но делать так конечно не надо).

ohmyfetch - универсальный fetch, работающий поверх undici

И большое количество других инструментов.

Все это очень вдохновляет, и также очень неожиданно - я не пользователь Nuxt, но подписан на оповещения о релизах.
Эти release notes обычно были достаточно скучные, и вообще не давали понять, какая работа ведется над фреймворком и экосистемой.

Очень жду, когда в swc и esbuild определятся с API плагинов, и появится похожая на unplugin инициатива, позволяющая абстрагировать модификации AST, подходящая и для babel, и для swc с esbuild. Если это вообще возможно :)
7🔥2👎1
Привет!

Прочитал статью про кэширование страниц SSR приложений - https://blog.vulcanjs.org/treat-your-users-right-with-http-cache-and-segmented-rendering-7a4f4761b549

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

В качестве механизма кэширования предлагается использовать возможности CDN и стандартные кэширующие HTTP заголовки, есть примеры сегментации с использованием Vary и ETag заголовков.
При этом рассказано про все недостатки использования заголовков (у Vary куча проблем), и самым гибким способом сегментации назван кастомный сервер, который сможет к примеру парсить куки, и влияющие на персонализацию или A/B тесты добавлять в заголовки, которые уже можно добавить в Vary.

Хорошие примеры про Vary есть у Fastly, вместо кастомного сервера можно конфигурировать сам сервис для вашего контента - https://www.fastly.com/blog/best-practices-using-vary-header

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

Для меня очень интересна возможность кэшировать SSR на уровне edge CDN, и после статьи мысли сразу убегают в сторону "как это можно использовать".
Но затем понял, что гораздо важнее вопрос не где кэшировать, и не в нюансах HTTP заголовков, а что именно можно закэшировать.
Например, для начала, могу ли я, даже с внедрением сегментов, сохранить конкретную страничку любого нашего приложения просто в LRU кэше?
Ответом строгое нет, персонализация и тесты пронизывают приложения на разных уровнях, также надо учитывать частые изменения контента и релизы микрофронтов.

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

Примеры таких механизмов - куки feature toggle или dark theme, user-agent мобильного или десктоп устройства.
Отображение персональной информации, если это возможно, лучше просто переносить на клиент.
Но конечно редко когда все бывает так просто)

В таком случае, сегментация действительно сможет хорошо работать, а место и способ кэширования страничек отходят на второй план.
👍6🤔4
Привет!

В последнее время много занимались проблемой качественного tree-shaking кода библиотек, которые участвуют в сборке tramvai приложений.
Нашли в пакетах tramvai репозитория много проблемных кейсов, которые не позволяют terser вырезать не используемый код - декораторы после транспиляции, статические свойства у функций, использование функций из сторонних пакетов (любой compose, createToken и вызовы других методов будут для terser потенциальным side-effect).
Частные случаи таких проблем можно решить, например с помощью комментария /* @__PURE__ */, но это либо ручная работа, либо магия babel плагинов.

Мой коллега нашел удачное и общее решение проблемы tree-shaking в рамках трамвай репозитория - переложить больше работы на плечи webpack.
Раньше, трамвайные пакеты собирались в один файл, и для терсера доходил практически весь код пакетов, который он оптимизировал по своим возможностям.
Теперь, доработав нашу утилиту @tramvai/build мы собираем трамвайные пакеты с сохранением их структуры, по типу транспиляции через tsc.
Благодаря этому, вебпак может не включать в граф модулей не используемые файлы, что автоматически предотвращает попадание этого кода в бандл, даже если терсер бы не смог его вырезать.

В итоге получился такой набор рекомендаций для создания библиотек, хорошо поддающихся оптимизациям:

- Не используйте декораторы
- Не используйте статические свойства для react компонентов
- Делайте отдельную сборку с ES modules
- Разделяйте логику в пакетах по небольшим модулям
- Сохраняйте файловую структуру этих пакетов при сборке
- Добавляйте в package.json поле "sideEffects": false (или указывайте массив файлов, которые нельзя вырезать - "sideEffects": ["some-global.css"])
- Транспилируйте исходники в ES2019 код для ES modules и браузерной сборок (немного мотивации в статье по ссылке)
- Обязательно проверяйте tree-shaking ваших пакетов на дефолтной webpack production сборке, добавляйте комментарий /* @__PURE__ */ при необходимости

Утилита @tramvai/build из коробки дает вам сборку CJS и ES модулей, modern JS код, сохранение файловой структуры, поддержку отдельной browser сборки, и рекомендуется для наших внутренних команд для сборки любых пакетов, используемых SSR приложениями.

Кстати, референсом для @tramvai/build была отличная тулза microbundle

Большую часть этих советов можно найти в статьях:
- Гайд по tree-shaking в Webpack документации
- Статья "How To Make Tree Shakeable Libraries"
👍101🔥1
Привет!

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

Раз в месяц провожу общекомандные встречи на тему Web Performance, и попадаются интересные статьи, хочется делать обзоры на некоторые из них.

Тема на сегодня - Web Vitals метрика Largest Contentful Paint, и парочка отличных статей, которые мы рассмотрим:

- Способы улучшения LCP от Calibre - https://calibreapp.com/blog/largest-contentful-paint
- Интересный подход к измерению клиентских метрик на примере LCP - https://csswizardry.com/2022/08/measure-what-you-impact-not-what-you-influence/
👍2🔥2
LCP - метрика скорости отрисовки самого значимого контента на странице, и в теории полезного для пользователя.
Как правило это некое самое большое изображение в видимой области страницы.

Первый инсайт из статьи - LCP помимо изображения может быть video элементом, или текстом, как правило это h1 или h2 заголовок.
Хороший пример - это Smashing Magazine, у которых в мобильной версии именно заголовок статьи является LCP, а до оптимизации была аватарка автора (и заодно отличная статья как Smashing Magazine улучшали Web Vitals - https://www.smashingmagazine.com/2021/12/core-web-vitals-case-study-smashing-magazine/).

Второй инстайт - LCP это комплексная метрика, и ее оптимизация состоит из множества этапов.
В первую очередь, для ускорения надо уменьшить LCP элемент, и соответственно уменьшить время до его отрисовки.

Что мы можем сделать для изображения:

- Выбираем лучший формат, webp или avif (тут поможет сервис imgproxy или тег picture)
- Прогоняем через оптимизаторы для уменьшения размера (сервис squoosh или imgproxy)
- Отдаем оптимальный размер с помощью srcset

Для видео:

- Формат mp4 и h264 кодек
- Компрессия и удаление аудио дорожки
- Используем CDN

Для текста:

- Формат woff2
- Self-hosted хранение шрифтов
- Очищаем от неиспользуемых начертаний и вариаций
- В идеале используем только системные шрифты

Следующая важная метрика для оптимизации, составная LCP - Time To First Byte, время до ответа нашего сервера:

- Используем более мощный сервер/хостинг по возможности
- Используем CDN
- Кэшируем все что можно с помощью cache-control
- Используем Server-Side Rendering / Static-Site Generation / Incremental Static Regeneration
- Мониторим производительность нашего SSR (время запросов к API, время на рендер, etc.)
- Обязательно измеряем TTFB метрику (с помощью https://zizzamia.github.io/perfume/ или https://github.com/GoogleChrome/web-vitals и таких сервисов как `Calibre`)

Затем проводим аудит приоритетов загрузки ресурсов нашего приложения:

- Добавляем preload нашего LCP изображения и/или файла шрифтов
- loading=”lazy” подходит почти для всех изображений, кроме LCP элемента, это уменьшит приоритет загрузки
- Используем подходящий font-display, рекомендуется swap, но может быть скачок контента при смене шрифтов, выбирайте лучший для вас вариант
- Повышаем приоритет важных ресурсов (используем Priority Hints - https://web.dev/priority-hints/, инлайн критичных ресурсов в HTML, добавляем defer для большинства скриптов)

И последнее, ускоряем Critical Rendering Path за счет ускорения блокирующих ресурсов:

- Минификация JS и CSS
- defer и async скрипты
- Critical CSS и code splitting
- Избавляемся от third-party скриптов, либо максимально откладываем их запуск. Также, можно вынести их в Web Workers с помощью partytown - https://partytown.builder.io/
🔥5👍31
Плавно переходим к следующей статье!

При измерении веб перформанса, и метрики LCP в частности, можно наблюдать большие погрешности, т.е. скорость работы сайт не детерменированная.
Это большая боль синтетических измерений перформанса в CI например с помощью lighthouse-cli.
Мне кажется, что такие замеры в принципе не жизнеспособны, но есть отличный пример Netflix, которые смогли, но это было очень не просто в реализации - https://netflixtechblog.com/fixing-performance-regressions-before-they-happen-eab2602b86fe

Отдельно отмечу про Real User Monitoring, метрики клиентский производительности тоже будут невероятно шумные, и имеют кучу нюансов (https://www.youtube.com/watch?v=OqbCNprhrlA&ab_channel=Фронтенд), но при наличии большого количества пользователей можно получить значимые результаты на каком-то большом временном отрезке.

Также, основные метрики Web Performance не атомарны, и состоят из множества других метрик, как мы уже разобрали на примере LCP.

Автор статьи “Measure What You Impact, Not What You Influence” выделяет несколько важных пунктов при улучшении и измерении перформанса.

Косвенные изменения

Для улучшения LCP можно сделать многое, даже не думая про LCP - ускорить редиректы, TTFB, critical rendering path, оптимизации изображений, self-hosted ресурсы.
При этом из-за различных погрешностей, мы скорее всего не увидим явное улучшение LCP после конкретной оптимизации, даже если на самом деле его улучшили.
Поэтому важно измерять именно то, что мы изменяем.

Изолированные изменения

С помощью User Timing API можно делать очень точные и изолированные измерения.
Пример, как измерять время загрузки наших стилей, что косвенно улучшит LCP:


<head>
<script>performance.mark('CSS Start');</script>

<link rel="stylesheet" href="app.css" />

<script>
performance.mark('CSS End');
performance.measure('CSS Time', 'CSS Start', 'CSS End');
console.log(performance.getEntriesByName('CSS Time')[0].duration)
</script>
</head>

Похожим образом можно измерять весь тег head, т.к. по сути он весь блокирует Critical Rendering Path.

Сигналы vs. Шумы

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

Но, на что вообще влияет приоритет загрузки ресурса?
Браузер не может загружать параллельно все ресурсы на страницы по ряду причин, и ставит их в очередь.
Приоритет влияет именно на время нахождения запроса в очереди.
В Network вкладке можно увидеть время Queueing запроса, и повышение приоритета с полутора секунд уменьшило его почти до нуля.

Таким образом, измерив именно то, что мы улучшили, мы можем быть уверены в этом изменении, точно так же, как уверены в том, что эта косвенная оптимизация улучшит другую метрику, в данном случае LCP!

Улучшайте метрики ваших приложений, измеряйте именно то, что вы изменяете, и используйте удобные инструменты.
🔥4👍21
Привет!

TLDR: Ищу коллегу в техническую команду!

Уже почти три года я работаю в Тинькофф в технической команде над SSR мета-фреймворком https://tramvai.dev

Мне всегда было интересно работать над классным продуктом, делать сложный UI, улучшать производительность и думать над архитектурой приложения.
В какой-то момент работы в платформенной команды онлайн-кинотеатра ivi произошел перекос в техническом направлении, во многом когда мы разрабатывали основу для серверного рендеринга React приложения ivi и писали первые страницы на новом стеке.

При поиске нового места работы я уже знал, что ищу техническую вакансию, что-то самое сложное и интересное из всех возможных вариантов.
Один из инсайтов при поиске - технические (или "core") команды это частое решение в больших компаниях, где надо масштабировать общие решения на сотни разработчиков, и редкость в компаниях где количество разработчиков под конкретную платформу не превышает один или два десятка человек.
Но такие компании есть, я все-таки попал в core команду, и мне до сих пор это очень нравится:

- практически каждый день приходится решать не тривиальные задачи
- мы поддерживаем фреймворк, которым пользуется больше двухсот фронтенд разработчиков - это крутое коммьюнити, Inner Source, совсем другие требования к обратной совместимости, тестированию и документированию кода
- на фреймворке работают более трех десятков приложений на сайте tinkoff - это налагает большие требования к поддержке и производительности, но при этом дает большое количество данных, метрик, возможность быстро получать результаты от экспериментов (но и цена ошибок высока, к сожалению)
- очень зрелые процессы в компании - активно пишем и обсуждаем ADR и RFC, полноценный Inner Source, проводим такие общие встречи как Web Performance и специфичные для React / Angular направлений
- возможность расти (в том числе прозрачная система грейдов) в сторону Tech, а не только Team - про это очень хорошо написано в докладе моего коллеги Саши Поломодова - https://apolomodov.medium.com/how-to-grow-if-you-are-a-senior-software-engineer-6ddd8edbebae

Оказывается, не так много разработчиков хотят пробовать свои силы в core командах, и мы все еще ищем сильного и заинтересованного коллегу для работы в первую очередь над двумя проектами:

- Фреймворк tramvai - работа над которым прокачает экспертизу в SSR, React, сборке frontend приложений, web производительности, мониторинге и производительности Node.js приложений, CI/CD процессах, организации сборки, публикации и версионирования сотни npm библиотек
- Консольная утилита unic - инструмент для простого деплоя приложений в k8s, использующийся десятками команд за рамками нашей фронтенд экосистемы. Это экспертиза в Kubernetes, Docker, инфраструктуре, тот широкий набор навыков, которого зачастую не хватает даже очень опытным frontend разработчикам

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

Достаточно подробно про наши процессы собеседований - https://github.com/Tinkoff/career/blob/main/interview/README.md

Если вас заинтересовала вакансия, пишите мне @SuperOleg39 или всегда можете оставить заявку на сайте tinkoff.ru/career/it
👍82😢2
Привет!

После релиза Next.js 13 было очень много интересных обсуждений, и одна из немногих фич, про которую я не слышал никакой критики - обновленный компонент next/image - https://nextjs.org/docs/api-reference/next/image.

Работать с изображениями в 2022 году действительно сложно, но и возможности у нас очень широкие.

По поводу сложности - когда мне понадобилось ознакомиться со всеми современными возможностями, в первую очередь это srcset и sizes, беглым поиском нашел около пяти гайдов, все подробные и с хорошей визуализацией (один из подробнейших - https://css-tricks.com/a-guide-to-the-responsive-images-syntax-in-html/), но даже изучив все я откровенно не разобрался во всех нюансах.

Но список возможностей впечатляет:

- Мы можем отдать пользователю изображение в оптимальном формате с помощью picture - форматы .webp и особенно .avif позволяют очень сильно уменьшить размер изображения
- Мы можем отдать оптимальное изображения для экранов разного размера и даже разной плотности пикселей с помощью srcset
- Самая сложная для меня часть, мы даже можем подсказать браузеру, на экране какого размера, какую часть этого экрана будет занимать изображение, с помощью sizes! Условно, на мобилках, весь экран, а на десктопе половину - это напрямую повлияет на выбор оптимального изображения из srcset
- Наконец-то, с очень хорошей браузерной поддержкой, мы можем легко объявить пропорции изображения передав атрибуты width и height или CSS свойство aspect-ratio. Т.е. больше никаких хаков с padding (https://css-tricks.com/aspect-ratio-boxes/), дополнительными невидимыми блоками, пустым svg изображением нужного размера (смотри исходники legacy next image компонента :) ).
Важный рецепт для хорошей метрики Cumulative Layout Shift.
- Адаптивная верстка стала проще, во многом благодаря aspect-ratio, object-position и object-fit
- Ленивая загрузка без сторонних либ с помощью атрибута loading="lazy"

Также есть ряд техник, как сделать placeholder изображение, например blur заглушка в base64, сгенерированная из исходной картинки.
Но не могу это отнести к развитию возможностей веба, кажется такие техники существуют уже давно, и зависят от инструментария сборки.
👍7
Не увидительно, что фреймворки стараются упростить эти вещи для пользователей, и различные Image компоненты можно увидеть у Next.js, Nuxt.js, Remix, SvelteKit, Gatsby, и даже в каком-то ограниченном виде в Angular (ограниченном, т.к. в рантайме у SSR фреймворков больше возможностей, чем у Angular на этапе сборки)

Такой компонент мы решили добавить и для нашего фреймворка tramvai

Документация к next/image во многом является спецификацией компонента, покрывает множество кейсов, и стала для меня отличным референсом.

Важная часть работы с изображениями - это оптимизация и конвертация в оптимальный формат.
В самом простом случае, можно сделать это вручную, с помощью таких сервисов как Squoosh.
Более продвинутый способ - оптимизация на этапе сборки, например с помощью image-webpack-loader.

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

Решить эти проблемы проще всего в рантайме, и тут я знаю два варианта:

- Использовать отдельный сервис для оптимизации изображений - например в Тинькофф мы используем замечательный https://imgproxy.net/
- Использовать библиотку для оптимизации на сервере самого приложения - например https://github.com/GoogleChromeLabs/squoosh

Работа с изображениями - это тяжелый вычислительный процесс, и кажется не очень разумным делать такое в рантайме SSR сервера, которому и React в HTML отрендерить не так просто.

Но на самом деле, проблема нагрузки актуальна и для отдельного сервиса, и для нее есть отличное решение - CDN.
Обработанные изображения можно агрессивно кэшировать, и в разы снизить нагрузку на приложение.

Важный момент по поводу кэширования - CDN должен учитывать кодировку изображения, если по одной и той же ссылке к примеру imgproxy отдаст .avif для Chrome или .webp для Safari.
Для этого надо передавать заголовок Vary: Accept в ответе с изображением, и настроить CDN на поддержку этого заголовка, в этом случае кэш будет сохраняться отдельно для каждого формата, и CDN будет отдавать подходящий формат с учетом заголовка Accept запроса из браузера.

Next.js обрабатывает изображений в рантайме, т.е. вся нагрузка приходится на приложение.
В первую очередь это удобно для разработчиков - не нужно поднимать и поддерживать отдельный сервис вроде imgproxy, все работает из коробки.
Второй момент, при деплое Next.js приложений через Vercel, ваши приложения будут находится за CDN, что позволяет кэшировать и страницы, и статику, и изображения, которые отдает приложение.
Для self-hosted деплоя придется проксировать запросы за картинками через CDN на приложение самостоятельно.

Скорее всего, Next.js также сохраняет изображения и страницы в файловой системе, и CDN тут только ускорит доставку, так далеко в исследовании я не заходил.

Хочется рассказать, что интересного было в разработке аналогичного решения.
👍4🔥2
Учитывая уже развернутый сервис imgproxy, я решил делать Image компонент поверх этого сервиса - это закрыло вопросы лишней нагрузки на SSR приложения, оптимизации под разные форматы, отдачи нужного формата в рантайме без тега picture (что позволяет сделать очень простую разметку).

Еще не решенная проблема этого выбора - невозможность использовать сервис при разработке, для изображений на localhost.

.avif формат у нас был отключен из-за высокой нагрузки при генерации изображений в этом формате, было интересно разобраться в проблеме, даже помогли немножко улучшить сервис - https://github.com/imgproxy/imgproxy/issues/938.
Оказалось, что для достижения сравнимого качества изображения, для AVIF надо указывать значительно более низкий уровень качества при оптимизации, хорошее сравнение форматов можно найти тут - https://www.industrialempathy.com/posts/avif-webp-quality-settings/

В итоге, после точечной настройки, мы подключили AVIF, и в среднем, с примерно одинаковым качеством, это снизило размер итоговых изображений на 30-50% по сравнению с webP.

Следующий кейс - автоматическое сохранение пропорций, вычисление aspect-ratio.
На примере некста, логику получения размера картинки можно встроить в сборку - https://github.com/vercel/next.js/blob/canary/packages/next/build/webpack/loaders/next-image-loader.js.
В tramvai я добавил простой лоадер, который переиспользует логику file-loader (компромисс, т.к. по хорошему надо использовать Asset Modules), но при именованном импорте отдает объект с данными об изображении, которые необходимы для сохранения пропорций:

jsx
import { image } from './image.png';

const { src, width, height } = image;

<img src={src} width={width} height={height} />

По аналогии с next/image, добавил в компонент несколько вариантов верстки (и без необходимости поддерживать старые браузеры и наличии aspect-ratio, верстка невероятно простая):

- адаптивная, но не больше размера изображения (intrinsic)
- резиновая под размер контейнера (responsive)
- фиксированный размер (fixed)
- заполняющая доступное пространство (fill)

Если для intrinsic и fixed изображений достаточно учесть только плотность пикселей на экране, и передать в srcset 1x и 2x изображения, то для оптимальной responsive и fill верстки srcset должен содержать изображения под совершенно разные размеры экранов.

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

Пример размеров из этого списка:

- 720px - популярная ширина мобилок 360px плюс 2x плотность пикселей
- 1366px - популярная ширина на десктопе
- 3840px - еще одна популярная ширина на десктопе, 1920px плюс 2x плотность пикселей

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

Еще из интересного, простые и удобные абстракции:

- Свойство quality - позволяет при необходимости сконфигурировать оптимизацию через imgproxy более высокого качества, или наоборот более низкого
- Свойство priority - по умолчанию делаем все изображения c loading="lazy", но можем повысить приоритет и убрать этот атрибут, а с самым высоким приоритетом автоматически добавляем preload link на страницу для этой картинки (тривиальная задача для SSR фреймворка), что идеально подходит для Largest Contentful Paint изображения

Это был очень интересный опыт, и интересно как дальше будет развиваться новый Image компонент.

Например, в будущем я планирую отвязать его от imgproxy, и перенести в список публичных пакетов, которые публикуются в open-source.

Также, надо будет выбрать хороший способ создания blur плейсхолдера для изображений.
👍7
Отдельно открыл для себя паттерн разработки, которым раньше пользовался неосознанно:

- Начинаю с ADR на новую фичу
- Далее создаю минимальное example приложение со всеми возможными кейсами
- Реализация фичи, вся разработка наглядно сразу в example приложении
- Практически из коробки получаем интеграционные тесты (в самом простом случае, снапшоты на HTML страничек example)
- И также есть готовые примеры кода, на основе которых легко сделать документацию

Спасибо за внимание!

Делитесь вашим опытом и мыслями в комментариях.
👍101
SuperOleg dev notes pinned «Привет! TLDR: Ищу коллегу в техническую команду! Уже почти три года я работаю в Тинькофф в технической команде над SSR мета-фреймворком https://tramvai.dev Мне всегда было интересно работать над классным продуктом, делать сложный UI, улучшать производительность…»