Start Debugging

Как ускорить медленный сервер анализа 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
separate4016.5 с / 20.7 с / 22.4 с1,225 / 1,291 / 1,067 МБ
workspace111.1 с / 11.6 с / 11.7 с499 / 495 / 490 МБ

Та же диагностика, тот же код, меньше половины памяти. В IDE разрыв больше, чем в разовом запуске CLI, потому что сервер живёт всю вашу сессию и каждый контекст остаётся в памяти.

Пошагово: снова делаем анализатор быстрым

  1. Обновите SDK, чтобы уйти от известных регрессий.
  2. Переведите репозиторий на pub workspace.
  3. Исключите сгенерированный и вендорный код в analysis_options.yaml.
  4. Удалите устаревшие плагины анализатора.
  5. Откройте корень 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:

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 показывает один контекст, а сгенерированный код исключён, оставшаяся стоимость приходится на реальный код. Что стоит проверить:

Это те же контексты, которые обходит dart fix, поэтому запуск dart fix по всему репозиторию тоже ускоряется после перехода на workspace. А если вы запускаете ИИ-агента против репозитория через MCP-сервер Dart и Flutter, он тоже общается с сервером анализа, так что более компактная раскладка контекстов помогает там не меньше, чем в вашем редакторе.

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад