Rust для JavaScript-разработчиков: владение, типы и где Rust полезен вебу
Введение в Rust для JavaScript- и TypeScript-разработчиков: владение и заимствование, Option и Result, перечисления и трейты, асинхронность, Cargo, WebAssembly и нативные модули Node.js, когда Rust оправдан для продукта.
JavaScript-разработчик сталкивается с Rust чаще, чем кажется, даже не написав на нём ни строчки. Многие инструменты, которыми он пользуется каждый день, всё чаще написаны на Rust: компилятор SWC, сборщики Rspack, Turbopack и Rolldown, линтер и форматтер Biome, набор инструментов Oxc. Причина одна — скорость без потери надёжности. В многолетних опросах Stack Overflow Rust стабильно лидирует по доле разработчиков, которые хотят продолжать с ним работать. Эта статья — введение в Rust для тех, кто пишет на JavaScript и TypeScript: чем Rust принципиально отличается, как понять владение и заимствование, как устроены ошибки, перечисления и трейты, как выглядит асинхронный код, где Rust даёт практическую пользу веб-разработке — от WebAssembly до нативных модулей Node.js — и когда его выбор оправдан для продукта, а когда нет. Чем Rust отличается от JavaScript Компилируемый язык со строгой статической типизацией. Большая часть ошибок, которые в JavaScript проявляются во время работы, в Rust не позволяет программе скомпилироваться. Нет сборщика мусора. Память освобождается детерминированно, когда владелец значения выходит из области видимости. Отсюда предсказуемая производительность без пауз на сборку мусора. Безопасность памяти гарантирует компилятор. Обращение к освобождённой памяти, гонки данных между потоками и нулевые указатели исключаются на этапе компиляции, если код не использует явно небезопасные блоки. Нет null и исключений. Отсутствие значения и ошибки выражаются типами Option и Result , и компилятор заставляет их обработать. Абстракции без накладных расходов. Итераторы, обобщённые типы и трейты компилируются в код, сопоставимый по скорости с написанным вручную. Цена — кривая обучения. Первые недели компилятор отклоняет код, который в JavaScript работал бы, и объяснения компилятора — хотя и очень подробные — поначалу кажутся спором с машиной. Переменные и типы let name = String::from("Анна"); // неизменяемая по умолчанию let mut count = 0; // изменяемая — явно count += 1; let price: f64 = 1200.0; let items: Vec u32 = vec![1, 2, 3]; let active: bool = true; Переменные неизменяемы по умолчанию, изменяемость объявляется словом mut . Это похоже на привычку писать const в JavaScript, только обязательную. Строки бывают двух видов: String — владеющая изменяемая строка в куче, и str — ссылка на строковые данные. Это первое, что путает JavaScript-разработчиков, и прямое следствие модели владения. Владение: главная идея Rust Три правила: у каждого значения есть один владелец; одновременно может быть только один владелец; когда владелец выходит из области видимости, значение освобождается. let a = String::from("данные"); let b = a; // владение перешло к b // println!("{a}"); // ошибка компиляции: a больше не владеет значением let c = b.clone(); // явная копия — два независимых значения println!("{b} {c}"); В JavaScript присваивание объекта копирует ссылку, и обе переменные указывают на один объект, который может быть изменён через любую из них. В Rust присваивание по умолчанию передаёт владение, а копирование делается явно. Простые типы вроде чисел копируются автоматически. Заимствование Чтобы использовать значение, не забирая владение, его заимствуют по ссылке. fn total_length(words: [String]) - usize { words.iter().map(|w| w.len()).sum() } fn add_word(words: mut Vec String , word: str) { words.push(word.to_string()); } let mut words = vec![String::from("rust")]; add_word( mut words, "wasm"); println!("{}", total_length( words)); Правило заимствования: в любой момент может быть либо сколько угодно неизменяемых ссылок, либо одна изменяемая. Это исключает целый класс ошибок, знакомых по JavaScript, — когда объект меняется в одном месте, пока другой код по нему итерирует, — и делает многопоточный код безопасным. Время жизни Компилятор следит, чтобы ссылка не пережила значение, на которое указывает. Чаще всего время жизни выводится автоматически, и явные аннотации нужны только в функциях и структурах, где связь между ссылками неочевидна. Option и Result вместо null и исключений fn find_user(id: u64) - Option User { /* ... */ } match find_user(42) { Some(user) = println!("{}", user.name), None = println!("Пользователь не найден"), } // Короче, со значением по умолчанию let name = find_user(42).map(|u| u.name).unwrap_or_else(|| "Гость".into()); use std::fs; fn read_config(path: str) - Result Config, AppError { let text = fs::read_to_string(path)?; // ? возвращает ошибку наверх let config = serde_json::from_str( text)?; Ok(config) } Оператор ? похож на await по ощущениям: одна буква, которая аккуратно пробрасывает ошибку вызывающему коду. Главное отличие от try/catch — сигнатура функции честно сообщает, что она может завершиться ошибкой, и вызывающий код не может это проигнорировать. Структуры, перечисления и сопоставление с образцом struct Order { id: u64, total: u64, status: Status, } enum Status { Pending, Paid { receipt_url: String }, Failed { reason: String, retryable: bool }, } fn describe(order: Order) - String { match order.status { Status::Pending = "Ожидает оплаты".into(), Status::Paid { receipt_url } = format!("Оплачен: {receipt_url}"), Status::Failed { reason, .. } = format!("Ошибка: {reason}"), } } Перечисления Rust могут хранить данные в каждом варианте — это прямой аналог размеченных объединений TypeScript, о которых мы писали в статье о продвинутом TypeScript . Сопоставление match обязано быть полным: забытый вариант — ошибка компиляции. Трейты и обобщения Трейт описывает поведение, которое может реализовать тип, — похоже на интерфейсы TypeScript, но реализацию можно добавить и к чужим типам. trait PaymentProvider { fn charge( self, amount: u64) - Result String, PaymentError ; } struct Sbp; impl PaymentProvider for Sbp { fn charge( self, amount: u64) - Result String, PaymentError { // ... Ok("txn-123".into()) } } fn pay P: PaymentProvider (provider: P, amount: u64) - Result String, PaymentError { provider.charge(amount) } Итераторы: знакомо по методам массивов // JavaScript const total = orders.filter((o) = o.paid).map((o) = o.total).reduce((a, b) = a + b, 0); // Rust let total: u64 = orders.iter().filter(|o| o.paid).map(|o| o.total).sum(); Разница под капотом: итераторы Rust ленивые и не создают промежуточных коллекций, а компилятор превращает цепочку в эффективный цикл. Асинхронный код Синтаксис async и await знаком, но в Rust нет встроенной среды выполнения асинхронного кода: её выбирают отдельно, чаще всего это Tokio. #[tokio::main] async fn main() - Result (), Box dyn std::error::Error { let body = reqwest::get("https://example.ru/api/health").await?.text().await?; println!("{body}"); Ok(()) } Для веб-серверов используют фреймворки вроде Axum и Actix Web, для работы с базами данных — SQLx с проверкой SQL-запросов на этапе компиляции, для сериализации — Serde. Cargo и экосистема Cargo — менеджер пакетов и система сборки в одном: зависимости, сборка, тесты, документация, форматирование и линтер из коробки. cargo new orders-service cargo add serde --features derive cargo test cargo clippy # линтер cargo fmt # форматирование cargo build --release Для JavaScript-разработчика опыт близок к npm, но с единым стандартным набором инструментов, о котором не нужно договариваться в каждом проекте. Где Rust полезен веб-разработке WebAssembly Rust — один из основных языков для WebAssembly. Тяжёлые вычисления в браузере — обработка изображений и видео, криптография, парсинг больших файлов, симуляции — выносятся в модуль WebAssembly, который вызывается из JavaScript. Инструменты wasm-bindgen и wasm-pack генерируют привязки и пакет для npm. Подробно о возможностях и ограничениях — в статье о WebAssembly . Нативные модули Node.js Библиотеки вроде napi-rs позволяют писать модули Node.js на Rust — для узких мест, где JavaScript упирается в производительность процессора. Именно так устроены многие современные инструменты сборки. Сервисы с высокими требованиями Сервисы с жёсткими требованиями к задержкам и потреблению ресурсов: прокси, шлюзы, обработка потоков данных, высоконагруженные API. Предсказуемое потребление памяти без сборщика мусора упрощает планирование ресурсов. Инструменты и приложения Утилиты командной строки компилируются в один исполняемый файл без зависимостей. Фреймворк Tauri позволяет делать настольные приложения с веб-интерфейсом и серверной частью на Rust. Когда Rust оправдан для продукта, а когда нет Оправдан узкое место производительности подтверждено измерениями и не решается оптимизацией на текущем языке; важны безопасность памяти и надёжность — системные компоненты, обработка недоверенных данных; вычисления в браузере, которым не хватает скорости JavaScript; в команде есть или планируется экспертиза в Rust. Не оправдан типичное веб-приложение с базой данных, где основное время уходит на ввод-вывод, а не на вычисления; продукт на стадии проверки гипотезы, где важна скорость изменений; команда без опыта Rust и без времени на обучение; рынок специалистов важен для роста команды — разработчиков на Rust заметно меньше, чем на JavaScript, Python или Go. Практичный путь — не переписывать систему, а выносить на Rust отдельные компоненты с измеримым выигрышем. Общие принципы выбора технологий — в статье о выборе стека . Как учить Rust JavaScript-разработчику Официальная книга «The Rust Programming Language» и упражнения Rustlings. Сначала владение, заимствование, Option и Result — без этого остальное не складывается. Небольшая утилита командной строки для реальной задачи: обработка файлов, выгрузок, журналов. Затем модуль WebAssembly или нативный модуль Node.js для проекта, который вы уже знаете. Асинхронность и веб-фреймворки — после уверенного владения основами. Читать сообщения компилятора полностью: в Rust они часто содержат готовое исправление. Высоконагруженные сервисы и производительные компоненты мы проектируем в рамках разработки веб-проектов . Частые вопросы Сложно ли перейти на Rust с JavaScript? Сложнее, чем на Go или Python: модель владения и строгий компилятор требуют перестройки мышления. Опыт TypeScript со строгой типизацией и размеченными объединениями заметно облегчает переход. Rust быстрее Go? Часто быстрее и предсказуемее по потреблению памяти благодаря отсутствию сборщика мусора, но разница зависит от задачи. Go проще в освоении и быстрее в разработке типовых сетевых сервисов. Выбор определяется требованиями и командой, а не бенчмарками. Можно ли писать веб-бэкенд на Rust? Да, фреймворки Axum и Actix Web зрелые и используются в продакшене. Но для типичного бизнес-приложения скорость разработки и доступность специалистов обычно важнее выигрыша в производительности. Заменит ли WebAssembly на Rust JavaScript в браузере? Нет. WebAssembly дополняет JavaScript для вычислительно тяжёлых задач, а работа с DOM, интерфейсом и большинством браузерных API остаётся за JavaScript. Что такое unsafe? Блок, в котором программист берёт на себя гарантии, которые обычно проверяет компилятор: работа с сырыми указателями, вызовы внешних библиотек на C. Он нужен в низкоуровневом коде и изолируется за безопасными интерфейсами. В прикладном коде обычно не требуется. Стоит ли учить Rust, если я не планирую на нём писать? Понимание владения, заимствования и явной обработки ошибок делает код на любом языке аккуратнее. Но если цель — практическая польза в работе, выгоднее углубиться в TypeScript и производительность JavaScript, а к Rust обратиться, когда появится конкретная задача.