AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发

写在前面

现在用 AI 写代码,已经不再是一件新鲜事。

我自己的工作方式也发生了变化:先让 AI 理解问题、修改代码,然后立刻让它验证结果;如果验证失败,再把错误交给 AI 继续修。这个循环通常是这样的:

text 复制代码
提出需求 → AI 修改代码 → dart analyze → 发现问题 → AI 继续修改

小项目里,这个循环非常舒服。可一旦项目变大,尤其是 Flutter 项目、插件较多的项目,问题就出现了:代码可能只改了一个文件,但每次 dart analyze 都要重新等待一轮分析。

对人来说,这是等待。

对 AI 来说,这是上下文被打断。

而当我同时让 AI 处理两个、三个任务,或者一个仓库拉出多个 Git worktree 时,问题会进一步放大:每个任务都可能启动自己的 analyzer,内存迅速上涨,分析时间也开始影响整个开发节奏。

这篇文章记录我对这个问题的分析,以及最后做出的一个实践方案:dart_analysis_daemon,命令行入口叫 dana。

AI 写代码以后,验证变成了高频操作

传统开发中,我们可能在一段时间内集中修改代码,最后再运行一次完整检查。

AI 辅助开发不是这样。

AI 每完成一小步,都需要尽快知道这一步是否成立:

  • 类型是否正确?
  • import 是否完整?
  • 生成的 API 是否真的存在?
  • analyzer plugin 有没有报错?
  • 当前修改有没有影响其他文件?

所以,分析器不再只是提交前的工具,而变成了 AI 编程循环中的实时反馈器。

这会带来两个直接后果:

第一,分析请求的次数显著增加。

第二,分析器的启动和初始化成本会被反复暴露出来。

特别是 Dart analyzer 并不是一个完全无状态的命令。它会建立分析上下文、读取 package 配置、解析依赖、加载插件,并持续维护文件和诊断状态。每次都启动一个新的 analyzer,相当于不断重复支付这些成本。

大项目里,真正慢的往往不是检查一个文件

很多人第一次遇到这个问题时,会尝试把命令改成:

bash 复制代码
dart analyze lib/foo.dart

这确实可以减少输出范围,但它不一定能消除 analyzer 的启动成本。

如果每次执行命令都重新启动 analyzer,那么即使目标只有一个文件,也可能需要经历:

  1. 启动 Dart 进程;
  2. 启动 analysis server;
  3. 读取项目配置;
  4. 扫描和建立分析上下文;
  5. 等待插件初始化;
  6. 最后才检查目标文件。

于是,真正需要优化的并不只是"这一次检查了多少文件",还包括"分析器是否可以被复用"。

我的第一个结论是:

对高频验证场景来说,持久化 analyzer 往往比单纯缩小检查范围更重要。

我没有修改 Dart analyzer 源码

一开始我也考虑过直接修改 analyzer 或 analysis server。

但这样做有几个现实问题:

  • 需要跟踪 Dart SDK 的版本变化;
  • 需要维护自己的 analyzer 分支;
  • Windows、macOS、Linux 的行为可能不同;
  • 发布和升级成本明显增加;
  • 最终用户需要使用一套特殊构建的 SDK。

这和我想解决的问题不匹配。

我更希望把优化放在 analyzer 外面:让官方 analyzer 继续负责分析,外层负责生命周期、复用、路由和资源管理。

最后采用的是一种 wrapper/daemon 思路:

text 复制代码
dana check
    ↓
连接已有的持久化 session
    ↓
把本次文件或目录检查请求发给 analyzer
    ↓
返回 JSON 诊断结果

如果 session 不存在,dana 才启动 analyzer;如果 session 还活着,后续请求直接复用它。

这也是 dart_analysis_daemon 这个名字的由来:它不是 analyzer 的替代实现,而是 analyzer 的生命周期管理层。

第一个收益:重复验证不再重复启动 analyzer

持久化 session 解决的是单个项目的重复启动问题。

第一次检查时,dana 启动 analyzer 并建立上下文;后续检查只发送增量请求。这样,AI 每次修改几行代码之后,都可以快速得到结果,而不用重新支付完整启动成本。

它还可以复用已有的 IDE analyzer。

如果 IDE 已经运行了 analyzer,dart_analysis_server_proxy 可以把 IDE 的 analyzer 暴露给 dana。这样命令行检查能够复用 IDE 当前的分析状态,包括已经加载的上下文以及部分未保存内容。

当然,这种复用也有边界:如果需要验证磁盘上的真实内容,仍然应该先保存文件;插件刚启动时,诊断也可能还没有准备好。

先看单 root:持久化 session 才是最大的第一步

在研究多个 worktree 之前,我先验证了最基础的单 root 场景:同一个项目,只让一个 analyzer 长期运行,看看 AI 反复验证时能省下多少时间。

这组数据来自一个真实的 Flutter 单体仓库:

  • 15,000+ 个 Dart 文件;
  • 约 70 个 package;
  • 2 个 analyzer plugin;
  • Apple Silicon,24 GB 内存。

对比的是每次都重新执行 dart analyze,以及 analyzer session 已经建立之后的 dana。

检查内容 每次独立执行 dart analyze dana,IDE 已开 dana,session 已预热
单个文件 约 3 分钟 不到 1 秒 约 0.5 秒
约 1,200 个文件的目录 约 8 秒,且缺少部分 plugin lint 不到 1 秒 约 0.5 秒
全项目检查 约 3.5 分钟 约 1 秒 约 0.5 秒
修改公共 API 后的全项目检查 约 3.5 分钟 约 30 秒 约 1 分钟

这组结果让我意识到,真正的收益并不是"让 analyzer 解析一个文件的算法突然变快",而是避免每次验证都重新初始化一套分析上下文。

不过这里必须把冷启动和热态请求分开:

  • 第一次启动 session,仍然要支付接近 dart analyze 的初始化成本;
  • analyzer plugin 第一次编译和加载,也仍然需要时间;
  • 只有 session 建立之后的重复检查,才会进入快速路径;
  • 如果公共 API 发生大范围依赖失效,热态检查仍然可能需要几十秒甚至更久。

在另一个 5,000 文件的 resolved-plugin 基准中,独立 dart analyze 约 39.62 秒;session 建立后,dana fast 约 2.47--5.06 秒,下降约 88%--94%。这类数据更接近 AI 高频修改单个文件、不断重复验证的工作流。

所以单 root 的结论是:

先把 analyzer 从"一次性命令"变成"长期运行的服务",通常就能解决大部分重复验证的等待问题。

第二个问题:多个 AI 任务会启动多个 analyzer

单个项目的 session 复用之后,我遇到的下一个问题是并行任务。

现在很多人的工作方式是:

  • 一个仓库同时拉出多个 worktree;
  • 每个 worktree 对应一个 AI 任务;
  • 每个任务都有自己的分析请求;
  • 任务之间可能同时运行。

例如:

text 复制代码
repo/
├── worktree-feature-a/
├── worktree-feature-b/
└── worktree-feature-c/

如果每个 worktree 各自启动 analyzer,那么内存消耗大致会变成:

text 复制代码
一个 analyzer × 三个 worktree

大型 Flutter 项目中,这很容易把机器推入换页,甚至让系统直接失去响应。

同仓库多个 worktree,共用一个 analyzer

dana 提供了一个显式 opt-in 的多根池:

bash 复制代码
DANA_MULTI_ROOT=1 dana check --root path/to/worktree-a
DANA_MULTI_ROOT=1 dana check --root path/to/worktree-b

第一个 worktree 启动 analyzer,后续兼容的 worktree 会把自己的 root 加入同一个 analyzer 进程。

这里有一个很容易误解的地方:

共用一个 analyzer 进程,不等于把多个 root 的分析上下文合并成一份。

每个 root 仍然会保留自己的分析状态。多根池主要节省的是:

  • analyzer 进程;
  • Dart runtime;
  • 部分全局缓存;
  • 多次启动和初始化的成本。

因此它不是"把三个项目变成一个项目",而是让一个分析 server 管理多个分析 root。

Windows 实测:内存下降,速度基本不变

我做了一组 Windows 对照实验。

内存实验使用两个轻量合成 worktree,每个 root 包含 20,003 个文件;延迟实验使用真实 Dart SDK 的 pkg/analyzer,不加载插件,先热身,再串行请求。

结果如下:

测量项 两个独立 worker 共享多根池 差异
两个 worktree 检查后的 analyzer Job 私有提交 2,464.2 MiB 1,434.9 MiB 节省 1,029.3 MiB,约 41.8%
热态单文件检查 p50 32.328 ms 32.908 ms 增加 0.580 ms
热态单文件检查 p95 36.045 ms 36.379 ms 增加 0.334 ms
热态 40 文件目录检查 p50 14.489 ms 14.637 ms 增加 0.148 ms
热态 40 文件目录检查 p95 17.822 ms 20.000 ms 增加 2.178 ms

这个结果给我的判断是:

在这组实验里,共享多根池明显减少了内存,但没有带来明显的热态请求速度损失。

不过,这组内存测试使用的是轻量合成源码,测量指标是 analyzer Job 的私有提交,不是 Dart heap,也不是完整工作集。因此我不会把 41.8% 直接宣传成大型 Flutter 项目的固定收益。

真实项目的收益取决于:

  • 项目文件数量;
  • 依赖规模;
  • analyzer plugin;
  • 每个 root 的分析状态;
  • 当前操作系统的内存回收行为。

内存保护:不能只让池无限增长

如果多根池允许无限加入 root,那么它只是把"多个进程打满内存"变成了"一个进程内部保留很多 root"。问题并没有消失。

因此我给多根池增加了几层保护。

1. root 数量上限

通过 DANA_POOL_MAX_ROOTS 限制一个池最多保留的 root 数量,默认值为 3。

当达到上限时,池会优先淘汰最久未使用、且允许淘汰的 root。主 root 和当前请求的 root 会被保护。

2. 内存准入

在 Windows 和 macOS 上,加入新 root 前会检查系统内存。

如果当前内存不足,池会尝试淘汰一个 LRU root;如果没有可淘汰的 root,就返回明确的准入失败,不会偷偷启动第二个独立 analyzer。

这点很重要。一个看似"友好"的回退行为可能造成更严重的问题:

text 复制代码
共享池加入失败
    ↓
又启动一个独立 analyzer
    ↓
内存压力进一步加剧

因此当前策略宁愿返回退出码 75,也不静默扩大内存占用。

3. 空闲 root 回收

池空闲时会定期采样系统内存。连续检测到压力后,只回收符合以下条件的 root:

  • 不是主 root;
  • 没有正在处理请求;
  • 已经空闲超过最小时间;
  • 不是当前请求保护的 root。

正在执行的分析不会被强制打断。

4. macOS 的 warn 与 critical

macOS 的内存压力不是简单的"有内存/没内存"。当前策略把 warn 视作提示,把 critical 视作强压力。

在多根池中,warn 不会直接导致新 root 被拒绝,也不会反复清空已经热身的 root;只有 critical 或物理可用余量确实低于保留值时,才进入多根池的压力处理。

全局单 worker 预算仍保持更保守的 warn 语义,因为它负责控制新的 analyzer worker 启动。

这是一种刻意的取舍:如果只看物理余量,系统可能已经在压缩内存或换页,但程序要等到 critical 才开始回收。它是尽力而为的保护,不是硬内存上限。

为什么没有默认开启多根池

多根池需要维护多个 root 的分析上下文,也会让内存行为更复杂。

所以它目前是显式开启:

bash 复制代码
set DANA_MULTI_ROOT=1

我不希望用户只升级版本,就突然改变 analyzer 的进程模型。尤其对于不相关的多个项目,自动发现和自动合并可能带来难以预期的资源竞争。

显式 opt-in 还有一个好处:当用户确实在使用多个 worktree 时,可以主动选择共享;当用户希望完全隔离时,也可以保持默认行为。

项目地址与快速开始

这篇文章涉及两个独立的 package,它们解决的是不同层面的问题:

安装 dana

bash 复制代码
dart pub global activate dart_analysis_daemon

安装后可以直接检查项目:

bash 复制代码
dana check --root path/to/project --files lib/main.dart

如果同一个仓库有多个 worktree,可以显式开启多根池:

bash 复制代码
# macOS/Linux
DANA_MULTI_ROOT=1 dana check --root path/to/worktree

# Windows PowerShell
$env:DANA_MULTI_ROOT = "1"
dana check --root path/to/worktree

安装 IDE analyzer proxy

bash 复制代码
dart pub global activate dart_analysis_server_proxy

proxy 通过 Dart-Code 的 dart.analyzerPath 接入 IDE,IDE 的 analyzer 暖机后,dana 可以复用这份分析状态。

两个项目的完整参数、环境变量和 IDE 配置见各自 README:

这套方案适合什么场景

我认为它特别适合以下场景:

  1. AI 高频修改、频繁验证的 Dart/Flutter 项目;
  2. 大型仓库中的增量检查;
  3. 同一个仓库的多个 Git worktree;
  4. IDE 已经运行 analyzer、命令行又需要检查的开发流程;
  5. 希望降低 analyzer 重复启动成本,但不想维护自定义 SDK 的团队。

它不适合被理解为:

  • analyzer 的替代品;
  • 完整的内存硬限制器;
  • 把多个 root 的上下文完全合并;
  • 在任意平台都能自动采样和控制内存。

我最后得到的结论

AI 编程把"代码分析"从低频动作变成了高频反馈环节。

如果每一次 AI 修改都重新启动 analyzer,那么大型项目的等待时间和内存成本都会被成倍放大。更合理的方向,是把 analyzer 当成长生命周期服务来管理:

text 复制代码
一次启动,持续复用;
多个 worktree,按需共享;
内存不足,明确拒绝或回收;
不修改 analyzer 源码,保持 SDK 可升级。

从目前的实验看,多根池最确定的收益不是"让一次分析变快很多",而是减少重复 analyzer 带来的内存和启动开销。

对于 AI 编程来说,这已经很有价值:当验证动作变得便宜,AI 的修改---验证---修复循环才不会被基础设施拖住。

结语

我现在更愿意把 analyzer 看成一个应该被复用的服务,而不是每次执行命令时临时启动的工具。

这也是我做 dana 的核心原因:让 AI 可以更频繁地验证代码,让大型 Dart/Flutter 项目的反馈循环保持在可接受范围内,同时把多任务、多 worktree 带来的内存风险控制在一个可解释的范围里。

这条路并不完美。内存采样有滞后,多根 root 也不会神奇地合并成一个上下文,macOS 的压力状态还需要在真实设备上持续观察。但至少,问题已经从"每次都重新启动 analyzer"变成了"如何管理一个长期运行的 analyzer 服务"。

我认为,这是更值得继续优化的方向。

相关推荐
season_zhu1 小时前
Skills For Real Engineers 系列之二:grill,把需求想清楚,也别把自己问进死胡同
ios·ai编程
鱼儿也有烦恼1 小时前
客户端应用架构模式
android·架构
独隅2 小时前
Claude 高效应用实战指南
android·java·开发语言·claude·claude code
硅基手札2 小时前
Android 系统全景专栏 00 · 总目录
android·操作系统·嵌入式开发
YB13752 小时前
Jetpack Compose 动画Crossfade
android
2401_833269302 小时前
Android歌词功能实现
android
恋猫de小郭3 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
YB13753 小时前
Jetpack Compose 动画AnimatedContent
android
美狐美颜SDK开放平台14 小时前
准备开发直播APP?视频美颜SDK接入前的十项检查清单
android·ios·音视频·直播美颜sdk·视频美颜sdk