283просмотров
84.2%от подписчиков
21 ноября 2025 г.
Score: 311
Давайте закончим неделю полезным материалом, поделюсь с вами интересным рабочим кейсом. Получил задачку связанную с необходимостью синхронизации состояния элементов интерфейса в разных компонентах. Пользователь кликает на своего рода карусель с навигацией и после этого скроллится сама карусель чтобы выбранная опция стала первой в списке, плюс вся страница скроллится к соответствующему разделу, выбранному в карусели. Также были другие сценарии при которых эта логика реагировала на изменение урла и другие действия пользователя. Все это было сдобрено диким легаси и даже классовыми компонентами. Нужно было аккуратно войти в этот ад и починить все не разломав по пути пол приложения. Самое простое решение в лоб - дождаться стабилизации ДОМ дерева и по таймауту сделать принудительную синхронизацию компонентов если она нужна. Минус такого решения состоит в том, что мы задаем жесткий интервал для срабатывания. При этом на дешевых девайсах указанной задержки все еще может не хватить для стабилизации приложения и проблема не будет решена, а производительные девайсы наоборот могут ждать лишнее время до выполнения функции по таймауту давно завершив все предыдущие действия. В поисках более правильного решения я задумался об использовании отложенных вызовов функции с помощью requestAnimationFrame или requestIdleCallback. requestAnimationFrame — это браузерный API, который позволяет выполнять функцию перед следующей перерисовкой экрана, синхронизируя обновление анимации с частотой обновления дисплея. requestIdleCallback — это браузерный API, который позволяет выполнять функцию, когда у браузера появляется свободное время между кадрами и основными задачами, не мешая плавности интерфейса. Кроме озвученных способов можно было посмотреть в сторону: React.useLayoutEffect - для выполнения перед браузерной отрисовкой, MutationObserver - можно дождаться стабилизации ДОМ-дерева, IntersectionObserver - для наблюдения за видимостью и позицией элементов, React.startTransition - для отложенных обновлений. В итоге, с учетом всех вводных, остановился на requestIdleCallback. Но и у этого решения есть недостатки. Во-первых этот метод еще не поддерживается в Safari (доступен только в экспериментальном режиме или нужен полифил). Во-вторых, известной проблемой является то, что слабые девайсы могут вообще не вызвать эту функцию или вызывать ее через десятки секунд, т.к. у браузера вообще не возникает времени простоя из-за повышенной нагрузки (благо проблему частично можно пофисксить используя таймер в доп параметрах вызова коллбека). В итоге после небольших дебатов примерно вот такой кусок кода поехал в прод. Обратите внимание на очистку таймеров чтобы избежать утечек памяти.
useEffect(() => { if (!scrollIntoViewOnMount) return let t1, t2 const idle = requestIdleCallback(() => { t1 = setTimeout(() => { ref.current?.scrollIntoView({ block: 'center' }) t2 = setTimeout(() => { forceShow.current = false window.dispatchEvent(new Event('scroll')) }, 100) }, 200) })
return () => { cancelIdleCallback(idle) clearTimeout(t1) clearTimeout(t2) }
}, [scrollIntoViewOnMount]) К сожалению в больших проектах постоянно приходится плодить костыли и соблюдать баланс между чистотой кода, бизнес требованиями и скоростью разработки. Всем хороших выходных!