惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
T
Tenable Blog
T
Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
Intezer
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
Darknet – Hacking Tools, Hacker News & Cyber Security
K
Kaspersky official blog
Security Latest
Security Latest
P
Privacy & Cybersecurity Law Blog
Google Online Security Blog
Google Online Security Blog
SecWiki News
SecWiki News
P
Palo Alto Networks Blog
TaoSecurity Blog
TaoSecurity Blog
Webroot Blog
Webroot Blog
Spread Privacy
Spread Privacy
O
OpenAI News
The Last Watchdog
The Last Watchdog
P
Proofpoint News Feed
C
Check Point Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
人人都是产品经理
人人都是产品经理
S
Security @ Cisco Blogs
Scott Helme
Scott Helme
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
月光博客
月光博客
S
Securelist
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
T
Troy Hunt's Blog
W
WeLiveSecurity
GbyAI
GbyAI
N
News | PayPal Newsroom
Y
Y Combinator Blog
C
Cisco Blogs
H
Help Net Security
The GitHub Blog
The GitHub Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 【当耐特】
Jina AI
Jina AI
MongoDB | Blog
MongoDB | Blog
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
云风的 BLOG
云风的 BLOG
小众软件
小众软件
N
News and Events Feed by Topic

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как я две недели дебажил React: история одного голосового
KondratevDen · 2026-04-23 · via Все публикации подряд на Хабре

Время на прочтение7 мин

Охват и читатели9

Бывает, что фича готова и её пора передавать в тестирование, но при этом не покидает ощущение, что что-то не так. Однажды внутреннее чутьё меня не обмануло и привело в исходники React.

Меня зовут Денис Кондратьев — я фронтенд-разработчик в Точка Банк. В статье поговорим, как работает приоритетный рендеринг в React, что такое проблема разрыва и разберём реальный кейс на примере корпоративного мессенджера.

Проблема с голосовыми

Всё началось с простой задачи — добавить голосовые сообщения в наш корпоративный мессенджер. Готовая фича должна была выглядеть так:

Пользователь записывает голосовое сообщение → Оно отображается в чате в виде аудиоплеера → Плеер показывает прогресс прослушивания

При этом у сообщения есть тред — отдельный экран с комментариями. По клику он загружает данные с бэкенда и, пока идёт запрос, крутится спиннер.

После написания кода я решил проверить основные сценарии. На первый взгляд всё работало как надо: голосовые сообщения записывались и воспроизводились, плеер показывал прогресс. Но после нажатия на тред интерфейс неожиданно завис в состоянии загрузки.

Судя по логам, бэкенд ответил со статусом 200, то есть данные пришли быстро, но спиннер всё равно продолжал крутиться. Тред открылся, только когда аудио проигралось до конца. Я решил разобраться, в чём причина.

Попытки дебага

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

start(): void {
   this.rafId = requestAnimationFrame(() => {
       this.setProgress(this._webApiPlayer.currentTime);
       this.start();
   });
}

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

constcurrentTime = useAudioPlayerProgress(audioController);

const progress = 100 / (duration / currentTime);

return <AudioPlayer progress={progress} />;

Затем — компонент активного треда, который обёрнут в Suspense и показывает спиннер, пока загружаются данные.

return (
   <Suspense fallback={fallback}>
       <ActiveThread />
   </Suspense>
);

На первый взгляд всё выглядело правильно: таймер обновлялся через requestAnimationFrame, данные для треда приходили, а Suspense показывал спиннер во время загрузки. То есть код выглядел слишком хорошо, чтобы быть причиной ошибки.

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

Как работает React

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

Если коротко, процесс рендеринга в React состоит из трёх этапов:

  • Фаза планирования. React получает обновления состояния, складывает их в очередь и решает, какие важнее.

  • Фаза рендера. Работает с виртуальным DOM, строит и обновляет дерево компонентов.

  • Фаза коммита. Взаимодействует с реальным DOM, т.е. интерфейсом пользователя.

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

Чтобы понять, как устроен этот механизм, нужно заглянуть чуть глубже в виртуальный DOM. Он представлен в виде дерева специальных объектов — Fiber.

Каждый такой объект содержит несколько групп полей:

  • Основные свойства: tag, key, type, elementType, stateNode.

  • Взаимосвязи в дереве: return, child, sibling.

  • Работа с обновлениями: pendingProps, memoizedProps, memoizedState.

  • Приоритеты: lanes, childLanes.

Алгоритм обхода дерева тоже не совсем привычный. React не идёт рекурсивно в глубину, как мы привыкли. Сначала он спускается к первому потомку (child), затем проходит соседние узлы (sibling), а чтобы вернуться к родителю, использует поле return. Благодаря этому он может в любой момент остановиться и продолжить с того же места.

Чтобы React решал, какую задачу выполнять следующей, у каждого Fiber есть два поля:

  • lanes — приоритет задач в текущем узле.

  • childLanes — приоритет задач у потомков.

Когда React доходит до очередного Fiber, он делает простую проверку. Если в lanes есть работа — выполняет её. Если более приоритетная работа, например, реакция на действия пользователя, находится в childLanes — спускается к потомкам.

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

Лейн (lane)

Приоритет

Функция или компонент

Sync Lane

Очень высокий

Обработчики событий useSyncExternalStore/useState

Default Lane

Высокий

Обновление без приоритета useState

Transition Lane

Средний

Функция startTransition

Retry Lane

Низкий

Механизмы Suspense

Idle Lane

Низкий

requestIdleCallback

Offscreen Lane

Низкий

Offscreen API

Гонка приоритетов

Так выглядел мой тестовый проект, на котором я дебажил React. Это наше виртуальное DOM-дерево: для удобства я убрал HTML-элементы и оставил только самые важные компоненты. Внизу добавил условные обозначения приоритетов:

  • зеленый — низкий приоритет;

  • красный — высокий приоритет;

  • синий — работа выполняется в данный момент.

Когда мы нажимаем кнопку «воспроизвести», меняется состояние таймера и бегунок начинает двигаться. Это приводит к обновлению компонента таймера. Поскольку внутри используется Sync External Store, оно получает высокий приоритет.

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

Так как других задач в данный момент нет, React берёт её в работу и спускается по дереву сверху вниз, начиная с root-компонента.

На каждом узле он проверяет Lanes и ChildLanes: если в lanes работы нет (то есть стоит значение NoLane), узел пропускается, и React идёт дальше. Так он доходит до каунтера и обновляет значение.

Важно, что на этом этапе изменения ещё не видны пользователю — всё происходит только в виртуальном DOM.

После завершения работы React поднимается обратно вверх по дереву, используя поле return, доходит до корневого узла и завершает фазу рендера.

Затем наступает фаза коммита, и только в этот момент пользователь видит изменения: ползунок в интерфейсе начинает двигаться.

Этот цикл повторяется много раз, пока таймер не закончится.

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

Так как в этот момент других задач нет, React берёт её в работу и снова начинает обход дерева сверху вниз.

Но в процессе выполнения возникает новая задача — обновление таймера. Она имеет более высокий приоритет, поэтому React делает следующее:

  • приостанавливает текущую (низкоприоритетную) работу;

  • возвращает её обратно в очередь;

  • переключается на выполнение высокоприоритетной задачи.

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

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

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

Тогда наш тред появится на экране.

Что в итоге произошло

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

Если посмотреть на issues в репозитории React, можно найти похожие описания. В частности, там говорится, что при использовании Concurrent Mode, startTransition или Suspense React может приостанавливать текущую работу, чтобы переключиться на более приоритетные задачи. Именно это мы и наблюдали в нашем кейсе.

Для этого даже есть специальный термин — голодание (starvation). Это состояние, когда задачи с низким приоритетом постоянно откладываются или не выполняются вовсе из-за непрерывного потока более приоритетных задач.

Вот что об этом пишет коллега из команды React:

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

Комментарий был создан ещё в 2022 году, но до сих пор ничего не изменилось.

Как убрать разрыв

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

  • Отказаться от React там, где нужна скорость. Теоретически это решает проблему, практически — тянет за собой много последствий. Мы используем готовую дизайн-систему, все компоненты которой написаны на React. Отказ от него означал бы, что часть интерфейса придётся переписывать, а потом ещё и поддерживать.

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

  • Использовать CSS. Это позволило бы вынести обновление таймера из React и убрать конкуренцию приоритетов. Но тогда нужно синхронизировать состояния между CSS и JavaScript. К тому же такой подход плохо сочетается с нашей дизайн-системой.

  • Использовать React Spring. Этот вариант выглядел перспективно, но так как сроки были ограничены, мы решили от него отказаться.

Были и другие радикальные идеи, типа canvas, SVG или SolidJS. Но, думаю, тут комментарии излишни. В нашем случае это выглядело как чрезмерное усложнение ради одной конкретной проблемы. Нам нужно было решение, которое впишется в текущую систему и не сломает остальную часть приложения.

Что мы сделали в итоге

В документации React в подобных ситуациях рекомендуют использовать useSyncExternalStore, то есть переводить обновления в максимально высокий приоритет (Sync Lane).

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

Тогда я решил копнуть в сторону React Scheduler. О нём довольно мало информации в сети, но идея звучит обнадёживающе: если низкоприоритетная задача слишком долго не выполняется, планировщик должен повысить её приоритет.

На практике всё оказалось не так радужно. Я снова начал записывать голосовые, но так и не дождался загрузки треда. Scheduler в нашем сценарии ничем не помог.

В итоге из всех вариантов сработал только один. Мы не стали повышать приоритет медленной задачи, а понизили приоритет быстрой. Предвосхищая вопросы — да, так можно было 🙂

Для этого мы обернули обновление состояния в startTransition. Под капотом это тот же useState, но с другим приоритетом: обновления переходят в Transition Lane (средний приоритет), а значит таймер и Suspense соревнуются почти на равных. И наконец всё заработало.

return useRxBind(
   playerProgress$.pipe(throttleTime(throttle)),
   startTransition:true
);

useRxBind — это кастомный хук, который подписывается на RxJS-стрим и синхронизирует его значение с состоянием React. Флаг startTransition: true говорит хуку оборачивать обновления состояния в startTransition, тем самым переводя их в Transition Lane.

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

В итоге:

  • голосовые сообщения продолжили нормально воспроизводиться;

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

Ирония в том, что на поиск причины ушло около двух недель, а само решение заняло две минуты 🙂