Иван Акулов про разработку @iamakulov_channel Channel on Telegram

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

@iamakulov_channel


JS · React · веб-перформанс · разработка и архитектура

Твитер: https://twitter.com/iamakulov
По всем вопросам (рекламу не продаю): @iamakulov
Чатик канала: @iamakulov_channel_chat

Иван Акулов про разработку (Russian)

Добро пожаловать в Telegram-канал Ивана Акулова про разработку! Если вы интересуетесь JavaScript, React, веб-перформансом, разработкой и архитектурой, то вы попали по адресу. В этом канале вы найдете полезные советы, инсайты, рекомендации и обсуждения о современных технологиях и лучших практиках в разработке. Иван Акулов - эксперт в данной области, который делится своим опытом и знаниями с подписчиками канала. Вы также можете следить за Иваном на Twitter по ссылке: https://twitter.com/iamakulov. Если у вас есть вопросы или предложения, обращайтесь к Ивану лично через Telegram: @iamakulov. Также присоединяйтесь к чату канала, чтобы обсудить темы, поделиться своим опытом и узнать что-то новое у единомышленников. Развивайтесь вместе с Иваном Акуловым и его каналом про разработку!

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

28 Feb, 10:30


Чёрт, снова вышел длинный пост вместо ссылок

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

28 Feb, 10:27


Компонент выше:
— говорит «запускай view transition», когда рендерится
— бросает исключение-промис, что заставляет Реакт ждать
— когда скриншот готов, резолвит промис, что позволяет Реакту закончить рендер

Если поместить этот компонент в конец дерева, то можно запустить view transition в конце рендера. Прямо как с useLayoutEffect, но асинхронно.

Попробуем это решение во Framer в течение недель. Расскажу, как сработает 🙂

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

28 Feb, 10:27


5) Если хуки не подходят, почему бы не запускать view transition перед началом рендера? Так можно, и Framer делает именно так. Но это создаёт другую проблему: рендер большой страницы может занять секунды, и всё это время страница будет заморожена.

6) Идеальных решений для этой проблемы сегодня нет. Но это не значит, что нет ужасных воркэраундов :D

Воркэраунд — эмулировать асинхронный useLayoutEffect с помощью <Suspense>:

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

28 Feb, 10:27


4) Удобное место, чтобы сказать «запускай view transition» — это хук useLayoutEffect. Этот хук вызывается, когда React уже закончил рендерить новую страницу, но браузер всё ещё показывает старую. Если запустить view transitions отсюда, то страница будет заморожена буквально долю секунды.

Идеально, да? Нет. «Запускай view transition» — это асинхронный процесс, а useLayoutEffect не поддерживает асинхронность (и вряд ли будет, исходя из архитектуры Реакта).

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

28 Feb, 10:27


View Transitions, React и перформанс
Ну и давайте про челленджи поговорим. Из недавнего — я узнал, что view transitions фризят страницу, и это влияет на перформанс-метрики, которые собирает Google 🫠

Следите за руками:

1) View Transitions — это новый стандарт, с помощью которого можно анимировать переходы между страницами. Framer поддерживает view transitions, и часть сайтов (в том числе framer.com) их используют

2) Переходы между страницами у Framer-сайтов работают как в любом реакт-приложении: вы нажимаете ссылку, а мы рендерим новую страницу Реактом. Это может быть медленно, поэтому, чтобы не фризить вкладку, мы оборачиваем рендер в startTransition(). (Вот как это работает)

Тут придёт Никита Прокопов и скажет, что вообще-то Реакт не нужен, и новую страницу будет гораздо быстрее отрендерить, просто взяв HTML с сервера. Это правда (но, кстати, не всегда — мы измерили!), и мы работаем над этим, но backwards compatibility и бэклог — сложно 😶‍🌫️

3) Проблемы начинаются, когда переходы между страницами анимируются с помощью view transitions. Вот как выглядит переход в этом случае:
— Вы говорите браузеру «запускай view transition»
— Браузер делает скриншот страницы и замораживает её целиком
— Вы рендерите новую страницу (реактом/вью/чем угодно)
— Вы говорите браузеру «всё, новая страница готова»
— Браузер анимирует переход

Заморозка страницы — это неприятно. Но ещё неприятнее то, что она совсем не встраивается в жизненный цикл Реакта.

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

28 Feb, 10:27


💻 Воркшоп (нет, канал я оживил не только для этого анонса): в марте пройдёт двенадцатый воркшоп по Реакт-перформансу! В консалтинге я помогал компаниям ускорять Реакт в 2-4 раза; в воркшопе я учу всему, чему сам научился в процессе.

Как выглядит воркшоп: мы берём медленное приложение → дебажим его в Chrome DevTools и React Profiler → находим и чиним боттлнек → повторяем для всех типичных перформанс-антипаттернов. В процессе мы смотрим на дорогие рендеры, гидрацию, startTransition(), перевычисления стилей и ещё пачку всего.

Приходите, цены на билеты вырастут с первого марта :) Отзывы и расписание вот тут → https://fwdays.com/en/event/react-performance-workshop-3

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

28 Feb, 10:27


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

🙋 Как дела: я закрыл консалтинг! Последние шесть лет я зарабатывал, консультируя компании (Google, CNBC, Toggl и кучу других) про веб-перформанс. Но в 2023-м мой консалтинг-драйв выгорел, и с 2024-го я ушёл в найм. Просуммировал выводы и уроки тут: https://iamakulov.com/notes/lessons-from-self-employment/

(Теперь я лидю веб-перформанс во Framer.com — это no-code-платформа для создания сайтов. Сайты на Реакте, поэтому перформанс-челленджи интересные :D)

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

04 Mar, 14:11


​​Show all events (линк)
Добавляет кучу дополнительных данных в перформанс-трейс. Включаю, когда хочется больше деталей про то, что именно происходит в браузере.

Бонус: если скопировать название ивента и вставить его в https://source.chromium.org/search, то этот сайт покажет, где именно в коде Chromium происходит этот ивент. Полезно, если нужно прям закопаться в поведение браузера

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

04 Mar, 14:11


​​Две самые любимые:

Event initiators (линк)
Добавляет стрелочки между setTimeout() и его колбеком, requestAnimationFrame() и его колбеком, и т.п. Супер-удобно, держу включённым по умолчанию:

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

04 Mar, 14:11


Собрал несколько незадокументированных фич в девтулзах, которые упрощают отладку перформанса: https://github.com/iamakulov/devtools-perf-features

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

13 Jan, 15:06


​​(Кстати, новый раунд React-перформанс-воркшопа начнётся уже на следующей неделе. Приходите: https://fwdays.com/en/event/react-rerformance-workshop)

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

13 Jan, 15:06


Что такое Immer (пропускайте, если знаете)
Immer — это библиотека для пэтчинга объектов. Например, у вас есть объект pokemon:

const pokemon = {
ownerId: 557,
details: { kind: 'Pikachu' }
,
evolutionHistory: []
}


и вы хотите сгенерировать новый объект с другим kind и evolutionHistory. В обычном JS это будет выглядеть длинно:

const evolvedPokemon = {
...pokemon,
details: {
...pokemon.details,
kind: 'Raichu',
},

evolutionHistory: [
...evolutionHistory,
{ from: 'Pikachu', evolvedAt: new Date() }
]
}


А с Immer это будет выглядеть гораздо компактнее:

const evolvedPokemon = immer.produce(pokemon, draft => {
draft.details.kind = 'Raichu'

draft.evolutionHistory.push({ from: 'Pikachu', evolvedAt: new Date() })
})


Короче, с Immer вы пишете код, который меняет объект напрямую — а Immer отслеживает все изменения и вместо того, чтобы менять старый объект, возвращает новый. (Под капотом это работает с помощью JS Proxies.)

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

13 Jan, 15:06


Но для перформанса кайф Immer в следующем:

Если записать в поля те же значения, которые были там до этого, Immer просто вернёт старый объект.

То есть, если взять userReducer с картинки выше и вызвать его с таким экшеном:

const user = { id: 123, status: { kind: 'online' } }
const action = {
type: 'UPDATE_USER_STATUS',
payload: { statusKind: 'online' }
}
const newUser = userReducer(user, action)


то Immer просто вернёт тот же объект user, который он получил:

console.log(newUser === user) // → true

Почему это круто? Прелставьте, что сервер три раза подряд прислал один и тот же статус пользователя.
— С обычным редьюсером объекты user и user.status поменяются три раза. Если в приложении есть компоненты, подписанные на эти объекты, то эти компоненты тоже перерендерятся три раза.
— С Immer-редьюсером объекты user и user.status поменяются максимум один раз. Компоненты, подписанные на них, тоже перерендерятся один раз.

Это круто, потому что что это убирает ненужные ререндеры.

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

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

13 Jan, 15:06


В воркшопе про React-перформанс у меня есть несколько любимых малоизвестных способов ускорить приложение.

Один из них — это переделать useSelector() в useStore() в Redux. Про него я писал в кейс-стади Causal: https://3perf.com/blog/causal/#useselector-vs-usestore

Другой — это подключить Immer в Redux-редьюсеры, Jotai-атомы и прочие места. Вот в чём фишка.

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

13 Jan, 15:06


​​Как Immer помогает с перформансом
Immer удобно использовать в Redux-редьюсерах, Jotai-атомах и т.д. С ним код получается компактнее + защищённее от случайных мутаций:

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

05 Jan, 01:55


React Concurrency под капотом

Наконец-то опубликовался доклад, с которым я выступал осенью на Smashing Conf и performance.​now(). Ловите :) https://3perf.com/talks/react-concurrency/

Внутри — куча картинок про новые перфоманс-фичи React 18:
⚛️ Как именно работает useTransition() (с демкой в девтулзах и просмотром кода из React-а, да)
🧊 Как применять <Suspense>, чтобы ускорить гидрацию, и как его не применять
🫠 Почему Vue.js и Preact отказались реализовывать что-то похожее на React Concurrency

(и другое)

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

16 Dec, 17:50


🖼️ <img srcset sizes="auto">

<img srcset> — это фича, с помощью которой браузер может выбрать подходящую картинку для устройства. Например, вот этот код:

<img srcset="/500.jpg 500w, /1000.jpg 1000w, /1500.jpg 1500w" sizes="500px">

загрузит 500.jpg на обычном экране и 1000.jpg на 2×-ретина-экране.

Чтобы эта фича работала хорошо, обычно также нужно указать ширину картинки в атрибуте sizes. Без sizes браузер будет считать, что картинка — шириной во всё окно браузера. Из-за этого будет загружаться слишком большой файл.

Но правильный sizes прописать сложно. Если на десктопе картинка шириной в 500 пикселей, а на мобильном — в 80% экрана, атрибут придётся сделать таким: sizes="(min-width: 768px) 500px, 80vw". Если условий больше, то атрибут будет ещё сложнее.

***

Так вот. Оказывается, Chromium начал экспериментировать с <img sizes="auto">. С таким атрибутом браузер будет рассчитывать ширину автоматически; сложные условия в sizes больше прописывать будет не нужно.

Но — такой атрибут работает только для изображений с loading="lazy".

Почему?
— Браузер не может рассчитать ширину изображения (и выбрать подходящий файл из srcset) без CSS
— Но — браузер не ждёт загрузки CSS, чтобы начать загружать изображения

Загрузки стилей ждут только изображения с loading="lazy". (Это из-за технических причин — без стилей браузер не может понять, во вьюпорте ли картинка и нужно ли её скачивать.) Поэтому и оптимизируются только они.

🔗 Ссылки:
— GitHub-ишью с изменением в стандарты: https://github.com/whatwg/html/pull/8008
— Chromium-баг: https://bugs.chromium.org/p/chromium/issues/detail?id=1359051

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

01 Dec, 12:49


Мой самый любимый дискавери из этого кейса — то, что веб-воркеры могут не только ускорить, но и замедлить приложение. [Веб-воркеры — это фича, с помощью которой можно взять код и вынести его в отдельный поток.]

У веб-воркеров много ограничений, но самое интересное — это то, что данные между главным потоком и веб-воркером всегда копируются (за парой исключений). Если данных много, то и копирование будет долгим.

У Causal в главном потоке было дорогое вычисление — парсинг данных (пик ↑). Мы попробовали вынести этот парсинг в веб-воркер, но вместо 800 мс на парсинг браузер начал тратить 4000-6000 мс на копирование данных.

¯\_(ツ)_/¯