В этой статье я расскажу, как я для себя понимаю архитектуру десктоп-приложений — и почему в какой-то момент Rust перестал быть «языком, который я когда-нибудь посмотрю», и стал практичным решением.

Сразу оговорюсь: это не попытка доказать, что «Electron — зло», а Rust — «спаситель». Это скорее дневник архитектора, который хотел собрать удобный продукт и по дороге несколько раз наступил на грабли. Некоторые грабли были с подогревом.

Немного контекста: как мы сюда пришли

Многие разработчики действительно начинали с фронтенда: HTML, CSS, JavaScript. Затем появились инструменты, которые последовательно снижали порог входа и повышали скорость разработки: jQuery, позже React/Vue/Angular.

Бэкенд тоже развивался: PHP эволюционировал (включая важный шаг к PHP 7), активно укрепились Python (Django) и Node.js. Для многих это упростило путь к «фуллстеку»: один язык, один стек, одна команда — и ощущение, что мир наконец-то стал понятнее.

Параллельно в мире десктопа долгое время доминировали нативные инструменты: Visual Studio для Windows, Xcode для macOS, Delphi (с наследием Turbo Pascal) и тому подобные . Плюс нативного подхода — богатые SDK и максимальная интеграция с ОС. Минус — кроссплатформенность. А когда Linux (во многом благодаря Ubuntu) стал заметнее как пользовательская платформа, вопрос «как делать одно приложение для всех» перестал быть теоретическим.

И вот на сцену выходят кроссплатформенные фреймворки: идея простая — пишем один раз, запускаем везде.

Electron: идеальный план… до первого серьёзного «но»

Electron выглядит как мечта: HTML/CSS/JS, знакомый стек, кроссплатформенность, быстрый time-to-market. Под капотом — Chromium + Node.js, отдельный процесс, доступ к системным API. На бумаге всё идеально.

Проблемы начинаются, когда приложение перестаёт быть «простой формой с таблицей» и в нём появляется:

  • нетривиальная бизнес-логика,

  • вычисления,

  • работа с файлами/криптографией/секретами,

  • требования по безопасности.

Да, Electron можно ускорять: выносить тяжёлое в нативные модули Node.js (C/C++), использовать worker-ы, оптимизировать рендеринг. Но дальше я упёрся не только в производительность.

Что у меня было на старте

  • Отличная идея продукта.

  • Проверенный стек.

  • Возможность расширения под тяжёлые задачи.

На MVP решение напрашивалось само: Electron. Быстро, понятно, «зато сделаем и проверим гипотезу».

И всё было неплохо ровно до момента, пока я не задумался о двух вещах:

  1. безопасность данных (и доступ к ним из пользовательской среды),

  2. защита бизнес-логики (хотя бы до разумного уровня).

И вот тут начались эксперименты.

Попытка №1: вынести логику в бэкенд и общаться по WebSocket

Классика: поднимаем локальный бэкенд, Electron становится оболочкой, общение — по WebSocket.

Технически это работает. Но по ощущениям — «костыль с гарантией». Почему:

  • канал связи по сути сетевой, даже если слушаем localhost;

  • придётся очень аккуратно думать про авторизацию, происхождение запросов и угрозы от локальных процессов;

  • защита «всё равно условная»: любой процесс в ОС может попытаться постучаться (да, можно усложнять жизнь атакующему, но магии тут нет).

Мы добавили защиту, стало лучше. Но внутреннее ощущение правильности решения так и не появилось.

Попытка №2: UDS / pipes вместо TCP + gRPC

Следующая идея: убрать TCP как класс и перейти на Unix Domain Sockets / named pipes. Логика понятная: локальный IPC, меньше «сетевых запахов», проще ограничивать доступ.

На практике я столкнулся с неприятными вещами:

  • потоковая передача работала нестабильно в моих сценариях;

  • ошибки диагностировались тяжело (особенно когда у тебя несколько процессов, разные рантаймы и «оно просто упало»);

  • отладка превращалась в отдельный вид спорта.

И снова: да, это можно довести до production, но цена «доведения» начала расти быстрее, чем ценность MVP.

Попытка №3: нативный аддон на C++ — надёжно, но больно

Затем я пошёл туда, куда Electron сам подталкивает в «серьёзных» случаях: native addon на C/C++.

Плюсы:

  • максимальная производительность;

  • полный контроль над взаимодействием с системой.

Минусы (лично мои):

  • мои знания C/C++ на момент эксперимента были недостаточно уверенными для быстрой и безопасной разработки;

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

  • ловля проблем уровня race condition и SIGKILL быстро превращается в ежедневную рутину.

Попытка №4: Go как компромисс (и как источник новых граблей)

Логика была простой: если C/C++ тяжеловато, возьмём то, что хорошо знаю и даёт скорость разработки — Go.

Тем более локальный бэкенд на Go у меня уже был: WebSocket и UDS-эксперименты не прошли зря.

В целом решение получилось рабочим. Но:

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

  • периодически вылезали race condition, падения, загадочные SIGKILL (и да, часть из этого — моя дисциплина и архитектурные решения, а не «плохой Go»);

  • сборка стала тяжелеть.

И финальный «контрольный выстрел»:

Первый релиз — около 500 МБ. Из них порядка 200 МБ — сам Electron (в зависимости от платформы/сборки цифры могли менять, но порядок оставался таким).

Для современного мира это не катастрофа, но для довольно простого приложения управления данными — неприятный осадок оставляет. Особенно если ты хочешь, чтобы продукт скачивали без ощу��ения, что вместе с ним ставится небольшой браузер. Хотя… ладно, не будем делать вид, что это не так.

Поворотный момент: Rust и Tauri

В какой-то момент я понял, что упёрся не в «как заставить Electron работать», а в архитектурную модель, которая постоянно требует обходных манёвров вокруг безопасности, ресурсов и поставки.

Я вспомнил про Rust — язык, до которого «никак не доходили руки». И подумал: если не сейчас, то когда.

И тут совпало сразу несколько вещей:

  • Rust даёт сильную модель безопасности памяти и дисциплинирует архитектуру.

  • Tauri позволяет оставить фронтенд (web-UI), но использовать системный WebView вместо встраивания полного Chromium.

  • Коммуникация между UI и ядром — внутри приложения, без ощущения «у меня тут локальный сервер торчит наружу».

План был быстрый:

  • фронт остаётся,

  • ядро на Rust,

  • старую Go-библиотеку сначала подключаем как subprocess (чтобы не переписывать всё сразу).

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

  • скорость работы стала заметно лучше (рендеринг 120 кадров в секунду даже дает неимоверные впечатления плавности),

  • размер дистрибутива — около 12 МБ (опять же, зависит от платформы и сборки, но порядок впечатляет).

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

Что я вынес из этой истории

  1. Electron отлично подходит для MVP, когда важнее скорость, чем идеальная инженерия. Но надо честно понимать цену: память, размер, безопасность и сложность при росте требований.

  2. Локальный бэкенд + WebSocket — быстрый путь, но без правильной модели доверия это вечный источник «а точно никто не постучится?».

  3. UDS/pipes + gRPC звучит красиво, но на практике может стать сложным в диагностике и сопровождении, особенно если вы строите систему из нескольких процессов.

  4. Нативные аддоны — мощно, но требуют компетенций и времени на качество.

  5. Rust + Tauri — хороший вариант, когда вам нужен web-UI, но вы не хотите тащить за собой целый Chromium и хотите более «собранную» архитектуру приложения.

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


Подписывайтесь на канал для получения информации от ИТ архитектора с более чем 20 летним стажем.