Старая архитектура: bridge и его узкие места
Классический вопрос уровня middle: что такое bridge, почему он был узким местом и как с этим жили.
Bridge — асинхронная очередь сообщений между JS-потоком и нативным миром в старой архитектуре React Native. Всё, что пересекало границу, сериализовалось в JSON.
Вопрос
«Что такое bridge в React Native, какие потоки в нём участвуют и почему это считалось узким местом производительности?»
Что на самом деле проверяет интервьюер
Знание bridge само по себе уже почти история: в актуальных версиях его заменила новая архитектура. Но вопрос задают, потому что через него видно, понимает ли кандидат причину целого класса проблем — почему нельзя дёргать нативный код синхронно, почему анимации выносят в useNativeDriver, почему длинный список тормозит и зачем существует Reanimated. Плюс во многих компаниях живут проекты на старой архитектуре.
Развёрнутый ответ
Три потока
- JS-поток — здесь выполняется ваш код: React, обработчики, бизнес-логика.
- UI-поток (main thread) — нативный поток, который создаёт view, обрабатывает касания и рисует кадры. Если его заблокировать, интерфейс замирает.
- Shadow-поток — считает layout по правилам Flexbox движком Yoga и превращает ваши стили в координаты и размеры.
Как работал bridge
JS не мог вызвать нативную функцию напрямую. Он клал в очередь сообщение «модуль X, метод Y, аргументы Z», аргументы сериализовались в строку JSON, пакет сообщений уезжал на нативную сторону, там десериализовался и исполнялся. Ответ возвращался тем же путём — через callback, тоже асинхронно.
// Псевдокод того, что происходило под капотом старого bridge
const queue = [];
function callNativeModule(moduleId, methodId, args) {
queue.push([moduleId, methodId, JSON.stringify(args)]);
}
// раз в кадр очередь батчем уезжает на нативную сторону
function flushQueue() {
nativeFlushQueueImmediate(queue);
queue.length = 0;
}
Отсюда три свойства старой архитектуры, которые надо назвать на собеседовании: асинхронность (синхронно получить значение из нативного кода нельзя), сериализация (всё превращается в JSON — строки и числа), батчинг (сообщения копятся и уезжают пачкой).
Почему это узкое место
Сериализация стоит времени и памяти, а пропускная способность очереди конечна. Проблемы начинались там, где через границу шёл плотный поток событий:
- Скролл и жесты. Палец двигается на UI-потоке, а реакцию считает JS. Событие уезжает через bridge, ответ приезжает уже к следующему кадру — картинка отстаёт от пальца.
- Длинные списки. Быстрый скролл требует создавать новые ячейки, а данные для них идут через ту же очередь. Отсюда классические «белые дыры» в
FlatList. - Тяжёлая работа в JS. JS однопоточный: длинный синхронный расчёт блокирует очередь целиком, и приложение перестаёт реагировать, хотя UI-поток свободен.
- Крупные данные. Передать через bridge base64-картинку — значит сериализовать мегабайты строки.
Как с этим боролись
import { Animated } from 'react-native';
// анимация целиком уезжает на нативную сторону один раз,
// каждый кадр через bridge уже не гоняется
Animated.timing(opacity, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
Идея всех обходных приёмов одна: не пересекать границу в каждом кадре. Отсюда useNativeDriver, обработка жестов в Reanimated на UI-потоке, getItemLayout у списков (чтобы не спрашивать размеры), InteractionManager для откладывания тяжёлой работы.
Важная оговорка
Bridge не «медленный» сам по себе: одиночный вызов стоит доли миллисекунды. Проблема в частоте и объёме — тысячи сообщений в секунду и невозможность синхронного ответа. Формулировка «bridge — бутылочное горлышко при интенсивном обмене» звучит гораздо профессиональнее, чем «bridge тормозит».
Типичные ошибки кандидатов
- Называют bridge «медленным» без объяснения, что именно дорого — сериализация и асинхронность.
- Путают потоки: считают, что React-рендер идёт на UI-потоке. Нет, рендер React — это JS-поток, а layout считает Yoga на shadow-потоке.
- Утверждают, что тормоза лечатся «оптимизацией JS». Часто дело не в скорости JS, а в количестве переходов через границу.
- Не могут привести ни одного практического приёма:
useNativeDriver, Reanimated, мемоизация ячеек списка.
Как ответить кратко
«Bridge — асинхронная очередь между JS-потоком и нативным. Всё, что через неё идёт, сериализуется в JSON и отправляется пакетами, синхронно получить ответ нельзя. Потоков три: JS, UI и shadow с Yoga для layout. Узкое место — не единичный вызов, а плотный поток событий: жесты, скролл, длинные списки, крупные данные. Отсюда все классические приёмы: useNativeDriver, Reanimated на UI-потоке, getItemLayout. Новая архитектура убирает именно это ограничение.»