Старая архитектура: 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. Новая архитектура убирает именно это ограничение.»

Проверьте себя
1. Как в старой архитектуре передавались вызовы между JS и нативной стороной?
AСинхронно, прямым вызовом функции по указателю
BАсинхронно, пакетами сериализованных в JSON сообщений через bridge
CЧерез общую память без сериализации
DЧерез HTTP-запросы на localhost
2. Почему обработка жеста в JS-обработчике онScroll могла давать «дёрганую» анимацию?
AПотому что JS-поток слишком медленный для арифметики
BПотому что каждое событие проходило асинхронный round trip через bridge, и ответ приходил уже к следующему кадру
CПотому что нативный поток UI не умеет обрабатывать больше 30 кадров в секунду
DПотому что события скролла отправлялись по одному и не батчились