Как ускорить медленный сервер анализа Dart в VS Code для большого монорепозитория Flutter
Монорепозиторий с десятками файлов pubspec.yaml заставляет сервер анализа Dart строить по контексту анализа на каждый пакет, и именно туда уходят память и минутный запуск. Переведите репозиторий на pub workspace, исключите сгенерированный код в analysis_options.yaml, откажитесь от устаревших плагинов анализатора и подтвердите результат на странице Insights. Измерено на Dart 3.12.2: в 2.3 раза меньше пиковой памяти и вдвое меньше времени холодного анализа.
Короткий ответ: в монорепозитории Flutter сервер анализа Dart медленный в основном потому, что создаёт отдельный контекст анализа для каждого пакета, у которого есть собственные pubspec.yaml и .dart_tool/package_config.json, и каждый контекст загружает свою копию SDK, Flutter и всех общих зависимостей. Превратите репозиторий в pub workspace (Dart 3.6+), чтобы все пакеты разрешались в один общий контекст, исключите сгенерированный код через analyzer: exclude: в корневом analysis_options.yaml, удалите устаревшие плагины анализатора, такие как custom_lint, и открывайте в VS Code корень workspace. На синтетическом репозитории из 40 пакетов и 3,240 файлов это снизило пиковую память примерно с 1.2 ГБ до 0.5 ГБ, а холодный анализ с 17-22 с до 11-12 с, ещё до того, как дело дошло до сгенерированного кода.
Всё, что описано ниже, измерялось на Dart 3.12.2 (Flutter 3.44.8) на Mac с Apple Silicon. Текущая стабильная версия это Dart 3.13.3, поставляемая с линией Flutter 3.47; ключи конфигурации и описанное здесь поведение там не изменились, а в 3.13.2 вдобавок объявлена устаревшей старая система плагинов, от которой раздел 4 советует отказаться.
Почему один репозиторий превращается в сорок анализаторов
Сервер анализа (процесс за командой dart language-server, которую запускает расширение Dart-Code, и за dart analyze) организует работу в контексты анализа. Контекст это набор файлов с общим разрешением пакетов и общим набором опций анализа. Каждый контекст держит собственную разрешённую модель элементов для всего, что он видит, а для пакета Flutter это SDK Dart, весь пакет flutter и каждая транзитивная зависимость.
Когда вы открываете корень монорепозитория, в котором лежат apps/customer, apps/driver и 38 пакетов в packages/, каждый со своими pubspec.lock и .dart_tool/package_config.json, у сервера нет выбора: эти пакеты могут разрешать collection или riverpod в разные версии, поэтому он строит 40 контекстов и разрешает package:flutter 40 раз. Команда Dart прямо пишет об этом на странице о workspaces: открытие корня без workspaces “create separate analysis contexts for each package, increasing memory usage”. Давний трекинговый issue по исправлению, dart-lang/sdk#53874, ставит сокращение числа контекстов в центр работы над производительностью сервера.
Симптомы в VS Code знакомы: “Analyzing…” крутится минуту после открытия папки, автодополнение занимает секунды, подсказки автоимпорта отстают от набора текста, а на машинах с 16 ГБ сервер убивается и перезапускается.
Сначала измерьте, потом меняйте
Гадать здесь дорого, поэтому сначала получите две цифры.
В VS Code выполните Dart: Open Analyzer Diagnostics / Insights из палитры команд. Откроется диагностическая веб-страница сервера. Страница Contexts перечисляет все контексты анализа с их расположением, корнем workspace и количеством “added” файлов (ваших) и “implicit” файлов (файлов SDK и зависимостей, которые контексту пришлось подтянуть). Если вы видите по контексту на пакет, у каждого из которых тысячи implicit-файлов, проблема найдена. Страница “Memory and CPU usage” показывает, что удерживает процесс, а страница “Legacy Plugins” перечисляет изоляты плагинов, если они есть. Dart: Capture Analysis Server Timings записывает, какие запросы медленные, если жалоба на автодополнение, а не на запуск.
Для воспроизводимой цифры, которую можно снимать в CI или до и после изменения, используйте командную строку. dart analyze запускает тот же сервер анализа, а два скрытых флага (видны через dart analyze -h -v) делают его пригодным для бенчмарка:
# Dart 3.12.2. --cache points at an empty dir so every run is cold.
rm -rf /tmp/dart-cache
/usr/bin/time -l dart analyze --cache=/tmp/dart-cache .
# "maximum resident set size" in the time output is peak memory (macOS, bytes).
# On Linux use: /usr/bin/time -v dart analyze --cache=/tmp/dart-cache .
# Server-reported heap, printed only with JSON output:
dart analyze --cache=/tmp/dart-cache --memory --format=json . | jq .memory
Флаг --cache важен. Без него запуск переиспользует ~/.dartServer, и тёплые прогоны скрывают большую часть разницы, которую вы пытаетесь измерить.
Репозиторий, на котором я измерял
Чтобы получить цифры, не привязанные к кодовой базе одной компании, я сгенерировал монорепозиторий из 40 чистых Dart-пакетов, в каждом по 80 файлов библиотек, barrel-файл и path-зависимости на два предыдущих пакета, так что граф зависимостей получается цепочкой, как в реальном многослойном приложении (core -> data -> features). Это 3,240 файлов. Пакеты Flutter ведут себя здесь так же; они просто делают каждый лишний контекст дороже, потому что package:flutter большой.
Два варианта: separate, где каждый пакет выполнял собственный dart pub get, и workspace, тот же код, переведённый на pub workspace. По три холодных прогона на каждый:
| Вариант | Конфигов пакетов | Холодный dart analyze | Пиковый RSS |
|---|---|---|---|
| separate | 40 | 16.5 с / 20.7 с / 22.4 с | 1,225 / 1,291 / 1,067 МБ |
| workspace | 1 | 11.1 с / 11.6 с / 11.7 с | 499 / 495 / 490 МБ |
Та же диагностика, тот же код, меньше половины памяти. В IDE разрыв больше, чем в разовом запуске CLI, потому что сервер живёт всю вашу сессию и каждый контекст остаётся в памяти.
Пошагово: снова делаем анализатор быстрым
- Обновите SDK, чтобы уйти от известных регрессий.
- Переведите репозиторий на pub workspace.
- Исключите сгенерированный и вендорный код в
analysis_options.yaml. - Удалите устаревшие плагины анализатора.
- Откройте корень workspace в VS Code и сократите то, что видит IDE.
1. Обновитесь, чтобы уйти от известных регрессий
Dart 3.11.0 вышел с проблемой производительности в workspaces с большим количеством файлов и каталогов, исправленной в 3.11.1 (dart-lang/sdk#62456). Есть и открытый отчёт, dart-lang/sdk#62704, о workspace из 18 пакетов, время анализа которого выросло примерно с 10 с до более чем 6 минут после перехода на 3.11.0. Если у вас ровно 3.11.0, сначала обновитесь и измерьте заново. В Dart 3.12 также ускорили запуск за счёт лучшего кеширования файлов опций анализа, что сильнее всего помогает, когда у каждого пакета свой analysis_options.yaml, который через include: подключает общий.
2. Переведите репозиторий на pub workspace
Для workspace каждый участник должен объявить resolution: workspace и нижнюю границу SDK не ниже 3.6. Корневой pubspec.yaml перечисляет участников:
# pubspec.yaml at the repo root. Dart 3.6+ (measured on 3.12.2).
name: _
publish_to: none
environment:
sdk: ^3.12.0
workspace:
- apps/customer
- apps/driver
- packages/core
- packages/data
- packages/design_system
# packages/data/pubspec.yaml
name: data
publish_to: none
environment:
sdk: ^3.12.0
flutter: ">=3.44.0"
resolution: workspace
dependencies:
flutter:
sdk: flutter
core:
path: ../core
Затем удалите артефакты разрешения зависимостей на уровне пакетов и выполните разрешение один раз из корня:
# Remove stale per-package lockfiles and package configs, then resolve the workspace.
find . -name pubspec.lock -not -path './pubspec.lock' -delete
find . -path '*/.dart_tool/package_config.json' -not -path './.dart_tool/*' -delete
flutter pub get # or: dart pub get
После этого остаются один pubspec.lock и один .dart_tool/package_config.json, оба в корне. Перезапустите сервер анализа (Dart: Restart Analysis Server) и снова загляните на страницу Contexts: там должен быть один контекст для workspace.
Компромисс в том, что у workspace единое разрешение версий. Если apps/driver закрепляет intl на одной мажорной версии, а apps/customer нужна другая, pub get будет падать, пока вы их не выровняете. Этот сбой и есть работа по миграции; большинство репозиториев обнаруживают два-три таких конфликта. Если вы используете Melos, версия 7.0.0 перешла на pub workspaces и заменила melos.yaml секцией melos: в корневом pubspec.yaml, так что обновление Melos и переход на workspace это одна и та же работа.
3. Исключите сгенерированный и вендорный код
Сгенерированный Dart часто занимает столько же места, сколько написанный вами код. Вывод freezed, json_serializable, mockito, drift и intl лежит рядом с исходниками в виде *.g.dart, *.freezed.dart и *.mocks.dart, и сервер разрешает и проверяет линтером каждую строку. (Если сама кодогенерация падает, смотрите несовпадение версий source_gen и analyzer, которое ломает build_runner; если вы выбираете между сгенерированными моделями и встроенными, подходящее сравнение это Dart records vs freezed classes.)
# analysis_options.yaml at the workspace root. Dart 3.12.2.
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.mocks.dart"
- "**/build/**"
- "third_party/**"
Я добавил по 10 файлов в стиле сгенерированных (около 1,000 строк) в каждый из 40 пакетов и снова измерил вариант с workspace:
| Workspace + сгенерированный код | Холодный dart analyze | Пиковый RSS |
|---|---|---|
| сгенерированные файлы анализируются | 19.4 с / 14.7 с | 1,148 / 1,185 МБ |
**/*.g.dart исключены | 6.9 с / 6.9 с | 523 / 523 МБ |
Четыре детали про exclude, которые документация не проговаривает, все проверены на 3.12.2:
- Глобы задаются относительно файла опций. Документация по анализу говорит это прямо.
**/*.g.dartработает откуда угодно;lib/**в корневом файле означаетlibкорня, а не каждого пакета. - В workspace exclude из корневого файла распространяется на участников, у которых есть собственный
analysis_options.yaml. В моём тесте у каждого пакета остался свой файл опций, иexcludeтолько в корне всё равно убрал их сгенерированные файлы из анализа. Без workspace это не работает: каждый пакет сам является корнем контекста, корневой файл для них игнорируется, и exclude нужен в каждом пакете, либо строкаinclude: ../../analysis_options.yamlв файле каждого пакета, которая переносит exclude. - Исключение файла не мешает его разрешению, когда кто-то его импортирует. Я исключил библиотеку, которую импортируют другие файлы, и поместил в неё предупреждение. Предупреждение исчезло, а импортирующие файлы по-прежнему проходили проверку типов. Значит, исключение экономит работу линтера и диагностики, а для файлов, которые никто не импортирует (моки, тестовые фикстуры, устаревший вывод), экономит всё, но
*.g.dart, являющийсяpartиспользуемой вами модели, всё равно будет прочитан. build/по умолчанию не пропускается. Папки, имя которых начинается с точки (.dart_tool,.git), игнорируются, но каталогbuild/илиios/Pods/, в котором случайно оказались файлы.dart, анализируется. Вложенные приложенияexample/со своимpubspec.yaml, не являющиеся участниками workspace, тоже становятся лишними контекстами. Либо добавьте их в workspace, либо исключите.
4. Удалите устаревшие плагины анализатора
Устаревшие плагины анализатора, те, что подключаются через analyzer: plugins: и используются custom_lint и более старыми инструментами, работают в отдельных изолятах, привязанных к контекстам анализа. Документация Dart предупреждает, что включение такого плагина “increases how much memory the analyzer uses”, и рекомендует вовсе избегать их, если у вас меньше 16 ГБ RAM или монорепозиторий с 10 и более файлами pubspec.yaml или analysis_options.yaml. Dart 3.13.2 формально объявил старую систему устаревшей.
Проверьте, есть ли они у вас:
grep -rn --include=analysis_options.yaml -A3 'plugins:' .
Если линты важны, переходите на новую систему плагинов, добавленную в Dart 3.10: она настраивается ключом plugins: верхнего уровня и поддерживается как IDE, так и dart analyze. В Dart 3.11 она стала переиспользовать AOT-снимок точки входа плагина, что, согласно changelog, экономит порядка 10 секунд в начале каждой сессии IDE. Если линты не настолько важны, чтобы их портировать, удалите плагин и измерьте разницу; обычно это самое большое снижение после перехода на workspace.
5. Откройте правильную папку и сократите то, что видит IDE
С workspace открывайте в VS Code корень репозитория, а не папку отдельного приложения, чтобы одна сессия сервера охватывала всех участников, а навигация между пакетами, переименование и поиск ссылок работали по всему репозиторию.
Две настройки Dart-Code стоит знать, а одной стоит избегать:
// .vscode/settings.json (Dart-Code extension)
{
// Folders the IDE analysis server ignores entirely, including for project detection.
"dart.analysisExcludedFolders": [
"tools/legacy_scripts",
"third_party"
],
// Keep SDK and dependency symbols out of Ctrl+T if workspace symbol search is slow.
"dart.includeDependenciesInWorkspaceSymbols": false
}
dart.analysisExcludedFolders влияет только на редактор, поэтому для всего, что должно пропускаться и командой dart analyze в CI, предпочитайте analyzer: exclude:. Настройка VS Code пригодится, когда папка должна анализироваться в CI, но не локально, например большое архивное приложение, которое никто в вашей команде не правит.
Избегайте dart.onlyAnalyzeProjectsWithOpenFiles. Она устарела, и её собственное описание предупреждает, что она “can make performance significantly worse when moving around a project”, потому что сервер постоянно разрушает и перестраивает контексты, пока вы переключаетесь между файлами.
Если всё ещё медленно
Если страница Contexts показывает один контекст, а сгенерированный код исключён, оставшаяся стоимость приходится на реальный код. Что стоит проверить:
- Циклические или очень широкие barrel-экспорты. Barrel-файл, реэкспортирующий весь пакет, делает каждого импортёра зависимым от каждого файла в нём, так что правка инвалидирует гораздо больше, чем нужно. Внутри пакета импортируйте по путям
src/, а barrel-файлы оставьте для публичного API. - Один мегапакет. Разбиение пакета
appна 3,000 файлов по фичам не уменьшает общий объём работы, но позволяет серверу не анализировать заново незатронутые библиотеки после правки. - Перезапуск сервера после крупных операций git. Переключение веток, затрагивающее сотни файлов, ставит в очередь много работы. Dart: Restart Analysis Server в некоторых случаях быстрее, чем ожидание инкрементальной инвалидации.
- Инструментирование для баг-репорта. Укажите путь в
dart.analyzerInstrumentationLogFile, воспроизведите проблему и приложите лог к issue в dart-lang/sdk. Регрессии, упомянутые выше, были найдены именно так.
Это те же контексты, которые обходит dart fix, поэтому запуск dart fix по всему репозиторию тоже ускоряется после перехода на workspace. А если вы запускаете ИИ-агента против репозитория через MCP-сервер Dart и Flutter, он тоже общается с сервером анализа, так что более компактная раскладка контекстов помогает там не меньше, чем в вашем редакторе.
Источники
- Pub workspaces (monorepo support), dart.dev
- Customizing static analysis, dart.dev
- Analyzer plugins, dart.dev
- Dart SDK CHANGELOG, записи для 3.10.0, 3.11.0, 3.11.1, 3.12.0 и 3.13.2
- Справочник настроек Dart-Code
- dart-lang/sdk#53874: reduce the number of analysis contexts
- Changelog Melos, 7.0.0
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.