Почему useSyncExternalStore проигрывает Zustand и Jotai на композитных сторах и как сел...
Почему useSyncExternalStore проигрывает Zustand и Jotai на композитных сторах и как селекторы убивают производительность Пытался вчера прикрутить useSyncExternalStore к композитному стору в React 19. Думал, что если это API из коробки, то и проблем с производительностью не будет. На деле — пришлось переписывать половину. Бенчмарк на 1000 подписчиков: примитивы vs вложенные объекты На простом сторе (один селектор, один примитив) разница в микросекундах — Jotai ~0.15ms, Zustand ~0.18ms, useSyncExternalStore ~0.22ms. Можно не париться. Как только в сторе появляется вложенный объект — начинается треш. Zustand без shallow перерисовывает всех подписчиков при любом изменении. Даже если твой селектор брал только state.user, а кто-то обновил state.posts. Причина: Zustand при каждом вызове селектора сравнивает результат через Object.is с предыдущим. Если селектор возвращает новый объект — привет, ререндер. Jotai с производными атомами — та же петрушка. atom(get => { return { user: get(userAtom), posts: get(postsAtom) } }) — создаёт новый объект на каждый вызов. Jotai также сравнивает через Object.is, так что ререндер гарантирован. Грабли с селекторами: деструктуризация убивает производительность Ошибка, которую я вижу на каждом проекте — деструктуризация в теле селектора: const { user, posts } = useStore(state => ({ user: state.user, posts: state.posts })) Каждый вызов селектора создаёт новый объект. Object.is никогда не вернёт true. Правильный подход — писать селекторы на отдельные поля: const user = useStore(state => state.user) const posts = useStore(state => state.posts) Тогда Object.is вернёт true, если ссылка не изменилась. Дополнительные меры: Zustand — используйте shallow для сравнения. Jotai — atomWithComparator. Экономия до 40% ререндеров. Когда useSyncExternalStore — зло для бизнес-логики useSyncExternalStore вообще не про бизнес-логику. Его пилили под внешние источники: WebSocket, IndexedDB. Внутри React он не батчит подписки, так что при 1000 подписчиков и частых изменениях — 1.2ms на ререндер. Jotai с мемоизацией — 0.3ms. Zustand с shallow — 0.6ms. Разница в 4 раза — это уже не погрешность. Сейчас на проекте простая архитектура: простые состояния — Zustand, меньше кода. Композитные данные — Jotai с atomFamily и splitAtom. useSyncExternalStore — только если прикручиваешь что-то внешнее. Вывод: Микрооптимизация селекторов и отказ от композитных возвращаемых объектов — это не premature optimization, а разница между 1.2ms и 0.3ms ререндера при масштабировании на 1000+ подписчиков.