← Вернуться в блог

Почему мы переписали Electron-приложение на Tauri/Rust

engineering tauri rust

Почему мы отказались от Electron

GitSquid v1 был построен на Electron. Он работал. Пользователи могли просматривать репозитории, индексировать изменения, управлять ветками, работать с удалёнными репозиториями и использовать встроенный терминал -- всё из одного десктопного приложения. Под капотом мы использовали simple-git для Git-операций, node-pty и xterm.js для терминала и стандартный стек React для интерфейса.

Но по мере развития приложения компромиссы Electron становилось всё сложнее игнорировать. Мы выпустили работающий продукт, но сам рантайм работал против нас. Поэтому мы переписали весь бэкенд на Rust с использованием Tauri 2.x -- сохранив при этом 95% нашего React-фронтенда.

Это история о том, почему мы это сделали, как мы это сделали и что сломалось по пути.

Проблема с Electron

Electron -- проверенный фреймворк. VS Code, Slack, Discord -- крупные продукты работают на нём. Но для Git-клиента накладные расходы непропорциональны задаче.

  • Размер приложения: Electron включает целый браузер Chromium и рантайм Node.js. Наше упакованное приложение весило более 150 МБ. Для Git-клиента это сложно оправдать.
  • Потребление памяти: Каждое окно Electron порождает несколько процессов Chromium. Потребление памяти в простое значительно превышало то, что потребовалось бы нативному приложению для той же работы.
  • Время запуска: Запуск Chromium не мгновенный. Пользователи замечали ощутимую задержку до того, как приложение становилось интерактивным, особенно на старом оборудовании.
  • Нативное ощущение: Несмотря на усилия Electron с нативными заголовками окон и меню, приложение никогда не ощущалось органичной частью ОС. Мелочи -- управление окнами, поведение фокуса, горячие клавиши -- всегда имели лёгкий привкус веб-приложения.

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

Почему Tauri 2.x

Мы оценили несколько альтернатив, прежде чем остановились на Tauri. Решающие факторы были очевидны:

  • Системный WebView вместо встроенного Chromium. Tauri использует WebView, предоставляемый ОС (WebKit на macOS, WebView2 на Windows, WebKitGTK на Linux). Уже одно это устраняет основную часть размера бинарника Electron.
  • Бэкенд на Rust. Бэкенд работает как скомпилированный бинарник Rust. Безопасность памяти, отсутствие сборщика мусора, предсказуемая производительность. Для инструмента, выполняющего десятки Git-операций за одно взаимодействие, это важно.
  • Кардинально меньшие бинарники. Приложение на Tauri может поставляться размером менее 10 МБ. Сравните с 150 МБ+.
  • Кроссплатформенность с нативной интеграцией. Tauri 2.x предоставляет надёжные API для нативных меню, системного трея, уведомлений и файловых диалогов. Приложение ощущается как часть ОС.
  • Активная экосистема и хороший опыт разработки. Система плагинов Tauri, модель IPC и документация значительно повзрослели с выпуском 2.x. Он был готов для продакшена.

Стратегия миграции

Полная переписка -- это риск. Мы снизили этот риск за счёт чёткого разделения ответственности.

Сохранить фронтенд, переписать бэкенд

Наш React-фронтенд уже был структурирован вокруг IPC-границы. Компоненты вызывали функции, которые общались с основным процессом Electron; нам нужно было лишь перенаправить эти вызовы на API invoke Tauri. Около 95% кода React перенеслось без изменений.

С бэкендом всё было иначе. Каждый Node.js-сервис -- Git-операции, наблюдение за файлами, управление терминалом, аутентификация, настройки -- нужно было переписать на Rust. В общей сложности мы написали около 7 200 строк на Rust и мигрировали более 120 IPC-команд.

Git CLI вместо libgit2

Частый вопрос: почему не использовать git2-rs (Rust-привязки для libgit2)?

Мы выбрали std::process::Command для прямого вызова Git CLI. Причины:

  • libgit2 поддерживает не все функции Git. Такие операции, как интерактивный rebase, некоторые стратегии merge и определённые параметры конфигурации, либо отсутствуют, либо ведут себя иначе.
  • У пользователей уже установлен Git. Вызывая их локальный бинарник Git, мы гарантируем полное соответствие поведению командной строки. Если это работает в терминале -- это работает в GitSquid.
  • Отладка проще. Команды, которые мы выполняем, -- те же самые, которые набрал бы пользователь. Когда что-то идёт не так, сообщения об ошибках знакомы.

Замена экосистемы Node.js

Каждая зависимость Node.js была заменена эквивалентом на Rust:

  • simple-git стал прямыми вызовами Git CLI через std::process::Command
  • node-pty стал portable-pty для эмуляции терминала
  • Хранение токенов перешло на крейт keyring, который использует системное хранилище ключей ОС (macOS Keychain, Windows Credential Manager, Linux Secret Service)
  • Наблюдение за файлами перешло на крейт notify
  • HTTP-запросы перешли на reqwest

Бесшовная миграция настроек

Мы сохранили ту же структуру директории конфигурации. Пользователи, обновляющиеся с v1 на v2, не теряют свои настройки, сохранённые репозитории или предпочтения. Бэкенд на Rust читает те же конфигурационные файлы, которые записывал бэкенд на Node.js.

С какими сложностями мы столкнулись

Миграция прошла не гладко. Несколько проблем потребовали значительного времени на отладку.

Нулевые байты при парсинге Git log

Мы используем пользовательские строки формата с git log --format для парсинга данных коммитов. В версии на Electron мы использовали определённые символы-разделители между полями. При портировании на Rust мы изначально попробовали использовать нулевые байты (\x00) в качестве разделителей полей -- они казались безопасным выбором, поскольку никогда не встречаются в сообщениях коммитов.

Ошибка. Строки в Rust обрабатывают нулевые байты без проблем, но несколько уровней стека трактуют их как терминаторы строк в стиле C. Вывод лога молча обрезался на первом нулевом байте. Мы перешли на многосимвольный разделитель, который никогда не встретится в выводе Git естественным образом.

Хранение токенов в системном хранилище ключей

Крейт keyring хранит учётные данные в системном хранилище ключей ОС, что является правильным подходом. Но мы столкнулись с аналогичной проблемой нулевых байтов: мы конкатенировали данные токенов с разделителями в виде нулевых байтов перед сохранением. При извлечении некоторые реализации системного хранилища ключей обрезали значение на первом нулевом байте, возвращая неполные учётные данные. Решение было тем же -- использовать видимый, однозначный разделитель.

Сложность аутентификации BitBucket

У GitHub и GitLab относительно простые потоки аутентификации на основе токенов. BitBucket сложнее. В зависимости от операции и типа аккаунта может потребоваться имя пользователя, email, пароль приложения или токен OAuth -- и правила, какой из них использовать и где, не всегда очевидны. Обеспечение надёжной работы аутентификации BitBucket для clone, fetch, push и API-вызовов потребовало нескольких итераций.

Построчное индексирование

GitSquid поддерживает индексирование отдельных строк из diff, а не только целых блоков (hunk). В версии на Electron генерация патчей происходила на JavaScript. Для версии на Tauri мы перенесли эту логику в бэкенд на Rust. Генерация валидного формата Git-патча из произвольного выбора строк, обработка крайних случаев с контекстными строками и обеспечение принятия результата командой git apply -- это была одна из самых сложных частей переписки. Реализация на Rust в итоге оказалась надёжнее исходной версии на JavaScript.

Французские апострофы, ломающие сборки

Этот случай запомнился. Некоторые из наших пользовательских строк содержали французские типографские апострофы (фигурная разновидность, U+2019) вместо прямых ASCII-апострофов. Они вызывали проблемы с кодировкой во время сборки при определённых конфигурациях CI. Исправление оказалось тривиальным после идентификации проблемы, но сообщения об ошибках указывали совсем не туда.

Результаты

После завершения миграции и достижения полного паритета функций вот что мы получили:

  • Размер приложения: значительно уменьшен по сравнению со сборкой на Electron. Пакет Tauri составляет лишь долю от тех 150+ МБ, которые мы поставляли раньше.
  • Потребление памяти: более низкий базовый уровень и более предсказуемое поведение под нагрузкой. Никакого дерева процессов Chromium, пожирающего оперативную память в фоне.
  • Время запуска: заметно быстрее. Приложение становится интерактивным практически мгновенно.
  • Паритет функций: 100%. Каждая функция из v1 присутствует в v2. Встроенный терминал, построчное индексирование, поддержка нескольких удалённых репозиториев, управление ветками -- всё на месте.
  • Повторное использование фронтенда: 95% кодовой базы React перенеслось, подтвердив правильность нашего архитектурного решения о сохранении чистой IPC-границы.

Стоило ли оно того?

Да.

Переписка потребовала значительных усилий. Около 7 200 строк на Rust, более 120 IPC-команд для повторной реализации и несколько неочевидных багов, которые проявились только потому, что Rust и Node.js обрабатывают граничные случаи по-разному. Это не был проект на выходные.

Но результат -- принципиально лучшее приложение. GitSquid v2 запускается быстрее, использует меньше памяти, поставляется в виде меньшего бинарника и ощущается более нативным на каждой платформе. Бэкенд на Rust более предсказуем и проще для понимания, чем эквивалент на Node.js. А поскольку мы сохранили тот же React-фронтенд, пользователи видят тот же интерфейс, к которому уже привыкли.

Если вы поддерживаете приложение на Electron и вас беспокоят накладные расходы рантайма, Tauri -- серьёзный вариант. Экосистема достаточно зрелая для продакшен-использования, а путь миграции -- особенно если у вас уже есть чистое разделение фронтенда и бэкенда -- более осуществим, чем может показаться при мысли о полной переписке.

Ключевой вывод: относитесь к IPC-границе как к API-контракту. Если ваш фронтенд умеет только вызывать invoke('get_commits', { path, branch }) и ему всё равно, приходит ли ответ от Node.js или Rust, замена бэкенда становится реалистичным проектом, а не фантазией.