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

С прошлой частью, где подробно описан функционал, можно ознакомиться здесь.

Интерфейс заметочника Aquilum, написанного полностью на Rust
Интерфейс заметочника Aquilum, написанного полностью на Rust

Ссылка на репозиторий проекта и последние релизы.

Первые версии 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 ядра и нативной части в репозитории.

Как шёл переход

Всё сразу не переписывалось, переход шёл поэтапно.

  1. Сначала я вынес всю логику приложения в отдельную библиотеку на Rust, независимую от Tauri и WebView. Это позволило заменить интерфейс, не переписывая остальную часть приложения.

  2. Затем сравнил два варианта интерфейса: собственное окно на winit с рендерингом через vello_cpu и готовый UI-фреймворк Freya. Тестировал одинаковый макет с боковой панелью, вкладками и текстовой колонкой в окне 2881 × 1717 при масштабе 175 %. Критерий был простым: пустое окно должно потреблять меньше 150 МБ оперативной памяти.

    vello_cpu — движок 2D-рендеринга, который превращает команды рисования в пиксели с помощью процессора, без использования видеокарты. Он используется в браузерном движке Servo для отрисовки Canvas.

  3. Для управления виджетами я выбрал masonry_core — ядро UI-фреймворка Masonry. Оно обрабатывает события и управляет деревом элементов интерфейса, но не создаёт окно и не отвечает за вывод изображения на экран.

    Freya на процессоре потребляла даже меньше памяти, чем мой вариант, но не подошла по другим критериям: использовала winit 0.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). С ней я находил и недорисованные области, и избыточную перерисовку.

Например, раньше при скролле перерисовывалась вся ширина редактора, а после внедрения подсветки только контентная часть.

Подсветка перерисовки в отладочной сборке (F12) в Aquilum
Подсветка перерисовки в отладочной сборке (F12) в Aquilum

Красным вспыхивают области, которые растеризатор перерисовал в этом кадре, фиолетовым — анимированная обложка.

Рамки: зелёная — область прокрутки, синяя — сдвиг пикселей при прокрутке, жёлтая — слой анимированной обложки, голубая — ползунок полосы прокрутки, который накладывается на готовый кадр без перерисовки страницы.

Свой редактор

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

Свой редактор MarkDown в заметочнике Aquilum
Свой редактор MarkDown в заметочнике Aquilum

Приёмы взяты из 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 МБ частной памяти: без этого приложение не выпускалось.

Потребление памяти у Aquilum наравне с другими приложениями в диспетчере задач Windows
Потребление памяти у Aquilum наравне с другими приложениями в диспетчере задач Windows

Что дало больше всего:

  • Картинки держатся, пока видны — кэш помнит картинку, пока она на экране, и ещё последние 8 МБ — как в Chromium. Раньше после закрытия всех вкладок в памяти оставалось около 80 МБ пикселей, теперь ноль.

  • Картинки декодируются сразу в нужном размере. PNG уменьшается строка за строкой, JPEG — внутри самого декодера, полноразмерная копия не создаётся.

  • Свёрнутое окно отдаёт память: буферы кадра, слои, недавние картинки. Раньше оно держало десятки мегабайт, ничего не показывая.

  • Вывод через WARP вместо драйвера видеокарты — минус около 85 МБ.

  • Граф выгружается вместе со своей вкладкой, а раньше закрытый граф держал до четырёх кадров по 20 МБ.

  • Поиску урезан аппетит: индексация шла на всех ядрах и доходила до 641 МБ, теперь ей дано 2 потока и 40 МБ.

Свои читалка, граф и PDF

Читалка открывает EPUB, FB2, MOBI и AZW3 без браузера: книга распаковывается, разбирается и раскладывается на страницы внутри приложения. Места чтения записываются в том же формате, что у прежней версии, поэтому старые закладки работают.

Читалка в заметочнике Aquilum
Читалка в заметочнике Aquilum

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

Граф в заметочнике Aquilum с настройками
Граф в заметочнике Aquilum с настройками

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)


  1. khomenkoev
    15.10.2026 06:05

    Салют.

    Не увидел, наверное быстро читал:

    1. Кого использовал из помощников?

    2. Как долго по времени это заняло? Прошлый вариант и текущий

    3. Темы не увидел))


    1. dima_yiu Автор
      15.10.2026 06:05

      Привет! Спасибо, что заметил — добавил в статью абзац:

      Переписывание приложения с Tauri на нативный UI на Rust заняло примерно 4–5 дней. В разработке мне помогали Claude Opus 5.5 и Gemini 3.8 Flash.

      Тем добавлял, странно...


  1. MikhailLukashov
    15.10.2026 06:05

    Спасибо, попробую использовать вместо VSCode

    А можно сделать PgUp/PgDown для прокрутки вверх/вниз? Ну и сделать остальные стандартные сочетания клавиш для навигации как в VSCode, или чтобы можно было это настраивать?

    Shift-Insert/Ctrl-Insert тоже не работают так как привычно


    1. dima_yiu Автор
      15.10.2026 06:05

      Спасибо большое за фидбек, сделаю это в ближайшем релизе!

      Если что-то еще заметите, что неудобно -- обязательно запишу себе и внедрю в будущих релизах