Я учел фидбек сообщества, открыл исходный код и сменил стек — теперь на Rust написано 99,6% всего кода, а от Tauri и Webview я полностью отказался. В статье расскажу, что внедрено и улучшено благодаря сообществу, а также какие проблемы возникли в процессе рефакторинга.
С прошлой частью, где подробно описан функционал, можно ознакомиться здесь.

Ссылка на репозиторий проекта и последние релизы.
Первые версии Aquilum были на Tauri: логика на Rust, а интерфейс — веб-страница в системном WebView. Один только WebView2 держит сотни мегабайт, и приложение с открытой заметкой и графом занимало около 650 МБ. В прошлой статье я написал про 9 МБ — это был замер без WebView, и в комментариях меня справедливо поправили.
В версии 0.3.0 WebView убран: весь интерфейс теперь на Rust. Переписывание заняло 4–5 дней, помогали Claude Opus 5.5 и Gemini 3.8 Flash.
Короткое определение проекта:
Aquilum — локальная база знаний из обычных markdown-файлов: вики-ссылки, граф связей, таблицы, работа с данными, запросы по свойствам заметок, мгновенный поиск, читалка книг (EPUB, FB2, MOBI, AZW3), история версий, экспорт в PDF и MCP-сервер, через который с базой работают ИИ-агенты. Лицензия AGPL-3.0, Windows, macOS и Linux.
Основные изменения
В 0.3.0 убран WebView. Интерфейс, Markdown-редактор, граф, читалка книг и экспорт в PDF теперь написаны на Rust и растеризуются на процессоре.
Что это дало:
память — порядка 70 МБ вместо 650 МБ (сценарии замеров разные, это порядок величины);
прокрутка тяжёлой заметки — 120 кадров в секунду с запасом: кадр 3–5 мс при бюджете 8,3 мс;
заметка в 10 МБ — первый кадр через 131 мс вместо 8 секунд;
Основные библиотеки проекта
Окно — winit, виджеты — Masonry, текст — parley, растеризация — vello_cpu, поиск — tantivy, база — SQLite, PDF — krilla.
Полный список крейтов с версиями — в Cargo.toml ядра и нативной части в репозитории.
Как шёл переход
Всё сразу не переписывалось, переход шёл поэтапно.
Сначала я вынес всю логику приложения в отдельную библиотеку на Rust, независимую от Tauri и WebView. Это позволило заменить интерфейс, не переписывая остальную часть приложения.
-
Затем сравнил два варианта интерфейса: собственное окно на
winitс рендерингом черезvello_cpuи готовый UI-фреймворк Freya. Тестировал одинаковый макет с боковой панелью, вкладками и текстовой колонкой в окне 2881 × 1717 при масштабе 175 %. Критерий был простым: пустое окно должно потреблять меньше 150 МБ оперативной памяти.vello_cpu— движок 2D-рендеринга, который превращает команды рисования в пиксели с помощью процессора, без использования видеокарты. Он используется в браузерном движке Servo для отрисовки Canvas. -
Для управления виджетами я выбрал
masonry_core— ядро UI-фреймворка Masonry. Оно обрабатывает события и управляет деревом элементов интерфейса, но не создаёт окно и не отвечает за вывод изображения на экран.Freya на процессоре потребляла даже меньше памяти, чем мой вариант, но не подошла по другим критериям: использовала
winit0.30 вместо необходимой мне версии 0.31 с обновлённой поддержкой IME, Skia вместо Parley для работы с текстом, а размер собранного бинарника составлял 22,7 МБ против 5 МБ у моего варианта.
Почему растеризация на CPU, а не GPU
Видеокарта стоит памяти, даже если она только показывает готовый кадр: её драйвер загружается в процесс приложения. Драйвер AMD занимал около 85 МБ — больше, чем всё приложение без него. Поэтому кадр рисует процессор, а на Windows его выводит WARP — программная реализация Direct3D от Microsoft, которой драйвер не нужен.
Цена — время процессора: на тяжёлой заметке кадр занимает 3–5 мс из 8,3 мс, отведённых на кадр при 120 Гц (Ryzen 9 8945HS). Кроме того, на пути через WARP изредка повторяются кадры, подробнее об этом в Плавности скролла. Зато код рисования один на всех трёх ОС.
Честно говоря, мне хотелось попробовать именно программную растеризацию: заметкам видеокарта почти не нужна.
Окно и кадры: что сделано поверх winit
Такт кадров
Кадр привязан к вертикальной синхронизации: отдельный поток ждёт IDXGIOutput::WaitForVBlank (как VSyncThreadWin в Chromium) и будит цикл событий, кадр рисуется один раз за такт. При выключенном мониторе WaitForVBlank возвращается сразу, поэтому такт ограничен периодом обновления, иначе выходило 200–300 кадров в секунду. Пропуски кадров считаются по GetFrameStatistics после каждого Present.
Вывод кадров
На Windows — цепочка DXGI с flip-моделью и тремя буферами. Процессор растрирует только изменённые области, и Present1 передаёт их списком. На macOS и Linux — softbuffer.
Плавность скролла
Больше всего времени я потратил на плавность прокрутки, и каждое изменение начиналось с замера. Колесо — порт кривой Безье из Firefox, тачпад — DirectManipulation со своей инерцией.
Мелкие рывки всё же остались, и эта проблема пока не решена. На пути WARP → DWM 1–6 % кадров показываются по два такта, а следующий сливается с ещё одним: прокрутка на мгновение замирает и догоняет скачком.
Простой
Кадры рисуются только по запросу. Каретка мигает по таймеру: 2 пробуждения в секунду вместо 120. Даже пустое пробуждение стоит около 0,27 мс процессора, так что кадр на каждый такт 120 Гц — это 30 мс в секунду впустую.
Почему пришлось форкать движки
В выпущенной Masonry нет рендера на процессоре, поэтому взята основная ветка, и из неё только ядро masonry_core: готовый хост окна умеет работать только с видеокартой.
Штатный адаптер рисовал эмодзи без учёта масштаба экрана, поэтому адаптер написан свой.
Masonry перерисовывает всё окно на каждом кадре: при вводе это около 14 мс, а при 120 Гц на весь кадр есть 8,3 мс.
Что изменено в vello_cpu и glifo
Новый глиф дорисовывается только в своём месте атласа. Раньше ради одного символа в одном потоке перерисовывалась вся страница атласа 4096×4096 — это и было главной причиной подтормаживаний.
Страница 512×512 вместо 4096×4096 (64 МБ), страниц не больше восьми, пустые освобождаются, кэши глифов чистятся раз в 30 секунд. Память (рабочий набор) на этом шаге: 145 → 84 МБ.
Что изменено в masonry_core
В кадр попадает только видимое, в то время как раньше на каждом кадре в сцену копировались все 2400 строк документа.
Смена темы без пересборки окна (
RenderRoot::restyle). Цвета и шрифты пересчитываются на месте, как в браузере. Смена цвета — около 2 мс вместо 15 мс плюс полной перерисовки, окно настроек больше не закрывается и не открывается заново.Профилировщик
aq_trace: Метки во всех этапах кадра Masonry и растеризатора, трасса открывается в Perfetto. Без него оптимизировать пришлось бы вслепую.
Что это дало
Вместе с работой над циклом кадра это подняло прокрутку тяжёлой заметки с 10 до 115–120 кадров в секунду, а удаление символа в большой заметке ускорилось с 81 до 1 мс (заметка 141 КБ, масштаб 175 %, 120 Гц, Ryzen 9 8945HS).
Как работает перерисовка только изменившегося
Для каждой команды рисования считается отпечаток и её прямоугольник на экране. Сравнение с прошлым кадром даёт до четырёх изменившихся прямоугольников: только они растрируются и только они уходят в окно. Тест попиксельно сравнивает такой частичный кадр с полной перерисовкой.
Мигание каретки перерисовывает только её место: кадр 1,2 мс вместо 16,9, процессор 1,8 %. Строка текста сравнивается кусками по 48 пикселей, поэтому раскрытие разметки рядом с кареткой не перерисовывает всю строку (так же сделано в Chromium и Firefox).
Уже нарисованные пиксели сдвигаются копированием, растрируется только открывшаяся полоса: кадр прокрутки 3,2 → 1,9 мс. Края областей округляются до целых пикселей экрана, иначе при масштабе 175 % сдвиг не срабатывал.
Форки, перерисовка только изменившегося и такт кадров вместе подняли прокрутку тяжёлой заметки с 10 до 115–120 кадров в секунду.
Чтобы видеть, что именно перерисовывается, сделана подсветка перерисовки (paint flashing, как в Chrome DevTools). С ней я находил и недорисованные области, и избыточную перерисовку.
Например, раньше при скролле перерисовывалась вся ширина редактора, а после внедрения подсветки только контентная часть.

Красным вспыхивают области, которые растеризатор перерисовал в этом кадре, фиолетовым — анимированная обложка.
Рамки: зелёная — область прокрутки, синяя — сдвиг пикселей при прокрутке, жёлтая — слой анимированной обложки, голубая — ползунок полосы прокрутки, который накладывается на готовый кадр без перерисовки страницы.
Свой редактор
Стандартное поле TextArea из Masonry на каждую букву заново раскладывает весь текст — около 15 мс на заметке в 141 КБ. Живому превью Markdown нужно больше: прятать разметку, пока в строке нет каретки, показывать таблицы и встроенные блоки. Поэтому редактор написан свой: текст раскладывается по абзацам через parley, разбор Markdown общий с ядром, а каретка и Backspace шагают по целым символам, включая эмодзи и буквы с ударением.

Приёмы взяты из CodeMirror 6 и VS Code, по их исходникам:
Текст раскладывается на экран выше и ниже видимой части, для остального хранится только высота. Полная раскладка занимала около 40 байт на байт текста: заметка в 10 МБ съедала 931 МБ, теперь всё приложение с ней укладывается в 107 МБ.
Высота дальних строк оценивается по числу символов, а место прокрутки привязано к позиции в тексте, как в CodeMirror 6, поэтому прокрутка не прыгает, когда оценка сменяется точной высотой.
Разбирается только изменённое, как в VS Code. Для каждой строки запоминается состояние разбора в её конце. После правки разбор идёт с места правки и останавливается, как только состояние совпало с прежним. Проходов по всему документу на нажатие было пять, стало ноль.
Абзац в памяти: 320 → 112 байт. При 461 тысяче абзацев каждые 100 байт — это 46 МБ.
На заметке в 10 МБ: первый кадр — 131 мс вместо 8 с, куча — 82–85 МБ вместо 194, нажатие клавиши — меньше 1 мс вместо 50.
Каждая оптимизация закреплена тестом, который сравнивает быстрый путь с медленным, например частичный разбор с полным. Чтобы проверить сам тест, условие временно ломается: тест обязан упасть.
Память
Цель — 50–60 МБ частной памяти: без этого приложение не выпускалось.

Что дало больше всего:
Картинки держатся, пока видны — кэш помнит картинку, пока она на экране, и ещё последние 8 МБ — как в Chromium. Раньше после закрытия всех вкладок в памяти оставалось около 80 МБ пикселей, теперь ноль.
Картинки декодируются сразу в нужном размере. PNG уменьшается строка за строкой, JPEG — внутри самого декодера, полноразмерная копия не создаётся.
Свёрнутое окно отдаёт память: буферы кадра, слои, недавние картинки. Раньше оно держало десятки мегабайт, ничего не показывая.
Вывод через WARP вместо драйвера видеокарты — минус около 85 МБ.
Граф выгружается вместе со своей вкладкой, а раньше закрытый граф держал до четырёх кадров по 20 МБ.
Поиску урезан аппетит: индексация шла на всех ядрах и доходила до 641 МБ, теперь ей дано 2 потока и 40 МБ.
Свои читалка, граф и PDF
Читалка открывает EPUB, FB2, MOBI и AZW3 без браузера: книга распаковывается, разбирается и раскладывается на страницы внутри приложения. Места чтения записываются в том же формате, что у прежней версии, поэтому старые закладки работают.

Граф раскладывает ядро, а рисует окно своим растеризатором: картинка делится на полосы, и полосы рисуются параллельно на всех ядрах процессора. Если, пока граф двигается, кадр выходит дольше 12 мс, граф временно рисуется в половинном разрешении.

PDF раньше печатал браузер: на macOS без установленного принтера получался пустой лист, на Linux открывалось окно печати. Теперь PDF записывается напрямую через библиотеку krilla: текст векторный и выделяется, принтер и браузер не нужны ни на одной ОС.
Видео играет через системные средства: Media Foundation и WASAPI на Windows, AVFoundation на macOS, GStreamer на Linux. На Linux программе нужны только GStreamer, GLib, fontconfig и FreeType, а ещё X11 или Wayland, которые есть в любой системе. GTK и WebKitGTK больше не нужны.
Что далось тяжело
Не всё прошло гладко, и часть проблем осталась.
На плавность прокрутки ушло больше всего времени, и мелкие рывки всё равно остались: на пути от программного вывода WARP к композитору Windows 1–6 % кадров показываются дважды, и прокрутка на мгновение замирает. С драйвером видеокарты повторов почти нет, но он стоит около 85 МБ памяти, и пока выбрана память. Проблема не решена.
Релиза Masonry с нужным рендером нет, поэтому обновления приходится забирать вручную и сразу пачкой.
В репозитории живёт 31 тысяча строк чужого кода, и исправления из оригиналов приходится переносить по одному.
Две копии библиотеки чтения шрифтов (для PDF и для окна) и библиотека SVG, которую Masonry подключает всегда, хотя его виджет SVG не используется. На память это не влияет, только на размер программы.
Данные пользователей мы не трогали: тот же каталог, те же форматы файлов и баз, тот же ключ подписи. Откат на 0.2.6 безопасен.
Заключение
Спасибо огромное всем тем, кто подсказал, куда можно двигаться проекту, а также отдельное спасибо тем, кто оставлял пул реквесты и исправления — почти все их я уже включил в последнюю версию Aquilum!
Предложить новую механику можно мне в личку в телеграм, но лучше делать это через pul requests или issues в репозитории.
В этой статье не затронуты аспекты UI и UX, которых было не меньше, чем технических нововведений, поэтому им будет отведена отдельная статья.
Что дальше:
убрать повторы кадров на пути WARP → DWM: передавать сдвиг прокрутки прямо в Present1 и попробовать вывод через DirectComposition;
верстать огромные главы книг по частям, чтобы FB2 из одной главы открывался так же быстро, как EPUB;
обновить parley и избавиться от второй копии библиотеки шрифтов;
заменить системное окно на Mac на нативную версию Aquilum.
Комментарии (4)

MikhailLukashov
15.10.2026 06:05Спасибо, попробую использовать вместо VSCode
А можно сделать PgUp/PgDown для прокрутки вверх/вниз? Ну и сделать остальные стандартные сочетания клавиш для навигации как в VSCode, или чтобы можно было это настраивать?
Shift-Insert/Ctrl-Insert тоже не работают так как привычно

dima_yiu Автор
15.10.2026 06:05Спасибо большое за фидбек, сделаю это в ближайшем релизе!
Если что-то еще заметите, что неудобно -- обязательно запишу себе и внедрю в будущих релизах
khomenkoev
Салют.
Не увидел, наверное быстро читал:
Кого использовал из помощников?
Как долго по времени это заняло? Прошлый вариант и текущий
Темы не увидел))
dima_yiu Автор
Привет! Спасибо, что заметил — добавил в статью абзац:
Переписывание приложения с Tauri на нативный UI на Rust заняло примерно 4–5 дней. В разработке мне помогали Claude Opus 5.5 и Gemini 3.8 Flash.
Тем добавлял, странно...