Skip to content

Latest commit

 

History

History
77 lines (42 loc) · 17 KB

File metadata and controls

77 lines (42 loc) · 17 KB

Хватит это терпеть: пишу свою IDE в браузере, которая не тормозит

Привет, Github!

Моя давняя мечта — создать свою веб IDE, погрузившись в процесс настолько глубоко, насколько это возможно. И я решил не идти на компромиссы. Я задался вопросом: а можно ли в 2025 году создать веб-IDE, которая будет и быстрой, и с идеально плавным, нативным скроллингом?

Так родился проект anycode. В этой статье я расскажу, как я решил эту дилемму.

Архитектура: Rust для скорости, TypeScript для интерфейса

С самого начала я решил разделить систему на две части:

  1. Бэкенд на Rust: Высокопроизводительный сервер, который берет на себя всю тяжелую работу: асинхронное взаимодействие с файловой системой, управление процессами языковых серверов (LSP) и запуск псевдотерминала (pty).
  2. Фронтенд на TypeScript + React: Современный и строго типизированный UI, который отвечает за отрисовку интерфейса.

Связь между ними осуществляется через WebSockets (Socket.IO), что позволяет в реальном времени обмениваться данными.

Что не так с готовыми решениями?

Изначально я начал с существующих решений — попробовал Monaco (сердце VS Code) и CodeMirror. Но оба варианта не устроили меня по разным причинам.

Monaco использует кастомный скролл для имитации прокрутки вместо нативного скроллинга. Как обсуждалось, например, на Hacker News в далеком 2016 году (ссылка на обсуждение), это делается для оптимизации отрисовки. Но у этого компромисса есть цена: скроллинг становится "неродным", дерганым и особенно ужасно лагает в Safari на Mac. У меня macbook m1 pro 120 герц и это очень заметно, не приятно пользоваться. о том почему лагает я провел исследование и пришел к выводу что не правильно рассчитывается deltaY в этом кастомном алгоритме (ссылка на исследование

CodeMirror, с другой стороны, имеет более плавный скроллинг, но его парсер для подсветки синтаксиса недотягивает до возможностей и скорости tree-sitter. Точность и производительность подсветки оставляют желать лучшего, особенно при работе с большими файлами и сложными языками. Я тестил на большом файле раст кода и при скролле наблюдал как парсер подрисовывет цвета - попросту не успевал.

Кто-то может упомянуть Zed — действительно нативный и быстрый редактор. Я не спорю с этим, но ключевое отличие в том, что Zed работает только как нативное приложение, а мне нужна была именно веб-версия, которая работает в браузере без установки. Веб-редактор открывает множество сценариев использования: удаленная разработка через браузер, быстрый доступ с любого устройства без установки (включая мобильные телефоны и планшеты), встраивание в образовательные платформы создание онлайн-сред разработки типа CodeSandbox или StackBlitz. По моему все еще нужен компонент редактора как часть другой системы.

Тогда я решил попробовать написать свой веб-компонент редактора с нуля, чтобы получить и нативный скроллинг, и мощный парсинг tree-sitter. Чтобы добиться и скорости, и нативности, я реализовал несколько ключевых техник в собственном движке рендеринга (эти идеи не новые, все уже придумано до меня).

1. Виртуализация: победа над лагами при редактировании

Многие думают, что виртуализация нужна в первую очередь для скроллинга. Это не совсем так. Современный браузер без проблем плавно прокрутит и тысячи простых div-элементов. Настоящий кошмар начинается при редактировании.

Представьте, что у вас в DOM отрендерено 10 000 строк, и вы добавляете один символ в самой первой строке, например началос строки = ". Браузеру придется пересчитать и перерисовать все 10 000 узлов, чтобы они стали "зелеными строками". Я проводил такой эксперимент: задержка после нажатия клавиши составляла несколько секунд. Работать так невозможно.

Именно эту проблему и решает виртуализация. Ограничивая количество DOM-элементов до небольшого видимого набора (viewport), мы гарантируем, что любое изменение затрагивает только этот маленький набор узлов. Редактирование становится мгновенным. А уже как следствие, мы реализуем и сам механизм виртуального скролла:

  • Viewport (Окно просмотра): В DOM единовременно существует лишь небольшое количество элементов строк (например, 100), которые физически видны пользователю.
  • Spacers (Распорки): Чтобы полоса прокрутки отражала реальный размер файла, я использую два div-распорки (сверху и снизу от видимых строк), высота которых динамически изменяется при скролле.

Но появляются проблемы у этого подхода - курсор и выделение удаляются при выходе из Viewporta. Тут мне пришлось писать дополнительный код который проверяет курсор каждый скролл, а для выделения посложнее - надо следить за его частями, иногда подрезать аккуратно верх или них, обрабатывать движения мыши, клики, двойные и тройные для выделения слов и строк, а также авто прокрутку страницы.

2. Нативный скроллинг и постепенная отрисовка

Вместо кастомного скрола Monaco я использую нативный скроллинг, но при этом динамически обновляя строки сверху и снизу. Но как именно управлять строками? При быстрой прокрутке может возникнуть идея добавить в DOM сразу батч строк (например, 20 строк). Такая операция может не уложиться в бюджет одного кадра (~16 мс и 8мс для 60 и 120Gz экранов), что приведет к дерганию и лагам, особенно на слабых устройствах.

Поэтому мой подход — постепенное добавление строк. Вместо того чтобы добавлять все 20 строк за раз, движок добавляет их по чуть-чуть, например, по 1-2-3 строки за кадр, используя requestAnimationFrame. Это распределяет нагрузку на несколько кадров и гарантирует, что каждый кадр отрисовывается сверхбыстро. Именно эта комбинация нативного скроллинга и постепенной отрисовки дает идеально плавное поведение без "дрожания" и лагов.

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

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

3. Инкрементальный парсинг с Tree-sitter

Для эффективной работы с текстом я использую библиотеку vscode-textbuffer, которая реализует структуру данных PieceTree. Эта библиотека оптимизирована для частых операций вставки и удаления, характерных для текстовых редакторов. PieceTree обеспечивает производительность даже при работе с очень большими файлами, эффективно управляя фрагментами текста без необходимости пересоздавать всю структуру при каждом изменении. Библиотека достаточно удобная и производительная, но если кто-то знает более оптимальные решения — буду рад почитать это в комментариях.

Для быстрой и точной подсветки синтаксиса я использую tree-sitter. Его главная особенность — инкрементальность. При вводе текста он не парсит весь файл заново, а лишь точечно обновляет синтаксическое дерево. Это позволяет иметь всегда актуальную подсветку максимально быстро. Парсинг происходит прямо в браузере с помощью tree-sitter-web и WebAssembly (WASM). Это означает. WASM обеспечивает высокую производительность, что очень важно для плавной работы редактора при редактировании больших файлов.

Для каждого языка программирования я создал отдельный query-файл, который описывает правила подсветки синтаксиса. Query использует специальный синтаксис tree-sitter для сопоставления узлов синтаксического дерева с семантическими категориями: функции, переменные, ключевые слова, строки, комментарии и т.д. Например, в JavaScript query определяет, что идентификаторы, начинающиеся с заглавной буквы, должны подсвечиваться как конструкторы, а встроенные функции вроде console или require — как function.builtin.

Особенно интересной возможностью является поддержка инъекций (injections) — механизма вложенной подсветки одного языка внутри другого. Без инъекций содержимое тегов <script> в HTML-файле автоматически НЕ парсится как JavaScript, а <style> — как CSS. Важно отметить, что tree-sitter из коробки не предоставляет такой функциональности — всю систему инъекций пришлось реализовывать самостоятельно. В query-файле паттерн с префиксом injection.content. указывает области, которые должны обрабатываться другим парсером. Система динамически загружает нужный парсер, парсит вложенный код отдельным синтаксическим деревом и применяет к нему query соответствующего языка.

Бэкенд на Rust: не просто файловый сервер

Мой бэкенд — это мозг операции.

  • Интеграция с LSP: Он выступает в роли "клиента" для настоящих языковых серверов (rust-analyzer, gopls и т.д.), что позволяет предоставлять "умное" автодополнение и диагностику.
  • Встроенный терминал: С помощью portable-pty я запускаю на сервере реальный псевдотерминал с bash или zsh. Весь ввод и вывод пробрасывается на фронтенд, где Xterm.js занимается его отрисовкой.

Благодаря использованию rust-embed весь фронтенд (HTML, CSS, JavaScript, WASM-файлы для tree-sitter) и конфигурация встраиваются прямо в бинарный файл на этапе компиляции. В итоге получается один самодостаточный исполняемый файл, который не требует внешних зависимостей или файлов для работы. Это значительно упрощает развертывание — достаточно просто скопировать один бинарник на сервер и запустить его.

Заключение

Создать быструю веб-IDE без компромиссов в UX — задача нетривиальная, но выполнимая. Ключ к успеху — не бояться отказаться от популярных, но компромиссных решений и переосмыслить узкие места. Комбинация Rust, TypeScript и умных алгоритмов позволила мне создать инструмент, которым действительно приятно пользоваться. Да, пока что это далеко до релиза и есть мелкие баги и проблемы, но мне кажется основа которую я заложил стоит того, чтобы продолжать разработку дальше и если подключится комьюнити, то из этого может получится хороший продукт.

Давайте обсудим его в комментариях!