跪拜 Guibai
← Back to the summary

Why `dart analyze` Is the Real Bottleneck in AI-Assisted Flutter Development

Why Dart Analysis Slows Down Large Projects in the AI Coding Era

Foreword

Nowadays, using AI to write code is no longer something new.

My own workflow has also changed: first let AI understand the problem and modify the code, then immediately have it verify the result; if verification fails, hand the error back to AI to continue fixing. This loop usually looks like this:

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

In small projects, this loop is very comfortable. But once the project grows larger, especially Flutter projects with many plugins, the problem appears: you might change only one file, but every dart analyze has to wait for a full round of analysis again.

For a human, this is waiting.

For AI, this is a context break.

And when I ask AI to handle two, three tasks at the same time, or pull multiple Git worktrees from one repository, the problem is amplified: each task may start its own analyzer, memory rises sharply, and analysis time starts impacting the whole development rhythm.

This post documents my analysis of this problem and the practical solution I finally came up with: dart_analysis_daemon, whose command-line entry point is called dana.

After AI Writes Code, Verification Becomes a High-Frequency Operation

In traditional development, we might modify code intensively over a period and then run a full check at the end.

AI-assisted development is not like that.

AI needs to know as soon as possible whether each small step is valid:

So the analyzer is no longer just a pre-commit tool; it becomes a real-time feedback mechanism in the AI coding loop.

This leads to two direct consequences:

First, the number of analysis requests increases significantly.

Second, the analyzer's startup and initialization costs are repeatedly exposed.

In particular, the Dart analyzer is not a completely stateless command. It sets up an analysis context, reads package configuration, resolves dependencies, loads plugins, and continuously maintains file and diagnostic state. Starting a new analyzer each time means paying those costs over and over.

In Large Projects, What's Actually Slow Is Often Not Checking a Single File

Many people first encounter this problem and try changing the command to:

dart analyze lib/foo.dart

This does reduce the output scope, but it does not necessarily eliminate the analyzer's startup cost.

If every command execution restarts the analyzer, even targeting a single file may require going through:

  1. Starting the Dart process;
  2. Starting the analysis server;
  3. Reading project configuration;
  4. Scanning and building the analysis context;
  5. Waiting for plugin initialization;
  6. Finally checking the target file.

So what really needs optimization is not just "how many files are checked this time," but also "whether the analyzer can be reused."

My first conclusion is:

For high-frequency verification scenarios, a persistent analyzer is often more important than merely narrowing the check scope.

I Didn't Modify the Dart Analyzer Source Code

At first I also considered modifying the analyzer or the analysis server directly.

But doing so has several practical problems:

That doesn't match the problem I want to solve.

I prefer to put the optimization outside the analyzer: let the official analyzer handle analysis, and the outer layer handles lifecycle, reuse, routing, and resource management.

The final approach is a wrapper/daemon idea:

dana check
    ↓
connect to an existing persistent session
    ↓
send the current file or directory check request to the analyzer
    ↓
return JSON diagnostic results

If the session doesn't exist, dana starts the analyzer; if the session is still alive, subsequent requests reuse it directly.

That's also where the name dart_analysis_daemon comes from: it's not a replacement implementation of the analyzer, but a lifecycle manager for the analyzer.

First Benefit: Repeated Verification No Longer Restarts the Analyzer

The persistent session solves the problem of repeated startup for a single project.

On the first check, dana starts the analyzer and builds the context; subsequent checks only send incremental requests. This way, after AI modifies a few lines, it can quickly get results without paying the full startup cost again.

It can also reuse an existing IDE analyzer.

If the IDE is already running an analyzer, dart_analysis_server_proxy can expose the IDE's analyzer to dana. This allows command-line checks to reuse the IDE's current analysis state, including loaded contexts and parts of unsaved content.

Of course, this reuse has boundaries: if you need to verify the real content on disk, you should still save the file first; right after a plugin starts, diagnostics may not be ready either.

Single Root First: Persistent Session Is the Biggest First Step

Before studying multiple worktrees, I validated the most basic single-root scenario: the same project, with only one analyzer running long-term, to see how much time is saved when AI repeatedly verifies.

The data comes from a real Flutter monorepo:

The comparison is between re-executing dart analyze every time and dana after the analyzer session is already established.

Check content Each independent dart analyze dana with IDE open dana with session warmed
Single file about 3 minutes less than 1 second about 0.5 seconds
Directory with ~1,200 files about 8 seconds, missing some plugin lints less than 1 second about 0.5 seconds
Full project check about 3.5 minutes about 1 second about 0.5 seconds
Full project check after changing a public API about 3.5 minutes about 30 seconds about 1 minute

This set of results made me realize that the real benefit is not that "the algorithm for analyzing one file suddenly gets faster," but that it avoids re-initializing a whole analysis context every time verification happens.

However, cold start and hot requests must be separated:

In another resolved-plugin benchmark with 5,000 files, a standalone dart analyze took about 39.62 seconds; after the session was established, dana fast took about 2.47–5.06 seconds, a reduction of about 88%–94%. This kind of data is closer to the AI workflow of frequently modifying one file and repeatedly verifying it.

So the single-root conclusion is:

Turning the analyzer from a "one-shot command" into a "long-running service" usually solves most of the waiting time in repeated verification.

Second Problem: Multiple AI Tasks Start Multiple Analyzers

After solving session reuse for a single project, the next problem I ran into is parallel tasks.

Many people now work this way:

For example:

repo/
├── worktree-feature-a/
├── worktree-feature-b/
└── worktree-feature-c/

If each worktree starts its own analyzer, memory consumption roughly becomes:

one analyzer × three worktrees

In large Flutter projects, this can easily push the machine into swapping, or even make the system unresponsive.

Multiple Worktrees in the Same Repository Share One Analyzer

dana provides an explicit opt-in multi-root pool:

DANA_MULTI_ROOT=1 dana check --root path/to/worktree-a
DANA_MULTI_ROOT=1 dana check --root path/to/worktree-b

The first worktree starts the analyzer, and subsequent compatible worktrees add their own root to the same analyzer process.

There's an easily misunderstood point here:

Sharing one analyzer process does not mean merging the analysis contexts of multiple roots into one.

Each root still keeps its own analysis state. The multi-root pool mainly saves:

So it's not "turning three projects into one project," but having one analysis server manage multiple analysis roots.

Windows Tests: Memory Drops, Speed Basically Unchanged

I ran a set of Windows control experiments.

The memory experiment uses two lightweight synthetic worktrees, each with 20,003 files; the latency experiment uses the real Dart SDK's pkg/analyzer, with no plugins loaded, warmed up first, then serial requests.

The results are as follows:

Measurement Two independent workers Shared multi-root pool Difference
Analyzer Job private commit after checking both worktrees 2,464.2 MiB 1,434.9 MiB saves 1,029.3 MiB, about 41.8%
Hot single-file check p50 32.328 ms 32.908 ms increase 0.580 ms
Hot single-file check p95 36.045 ms 36.379 ms increase 0.334 ms
Hot 40-file directory check p50 14.489 ms 14.637 ms increase 0.148 ms
Hot 40-file directory check p95 17.822 ms 20.000 ms increase 2.178 ms

The judgment I draw from this result is:

In this set of experiments, the shared multi-root pool clearly reduces memory, but it does not bring an obvious loss in hot request speed.

However, the memory test used lightweight synthetic source code, and the measured metric is the analyzer Job's private commit, not the Dart heap, nor the full working set. So I won't advertise 41.8% directly as a fixed gain for large Flutter projects.

The actual benefit for real projects depends on:

Memory Protection: Can't Just Let the Pool Grow Indefinitely

If the multi-root pool allows unlimited roots to join, then it's just turning "multiple processes exhausting memory" into "one process holding many roots internally." The problem doesn't disappear.

So I added several layers of protection to the multi-root pool.

1. Root Count Limit

DANA_POOL_MAX_ROOTS limits the maximum number of roots a pool can keep, with a default value of 3.

When the limit is reached, the pool evicts the least recently used root that is eligible for eviction. The main root and the root of the current request are protected.

2. Memory Admission

On Windows and macOS, the pool checks system memory before adding a new root.

If memory is insufficient, the pool tries to evict an LRU root; if there is no evictable root, it returns a clear admission failure instead of silently starting a second independent analyzer.

This is important. A seemingly "friendly" fallback behavior can cause a more serious problem:

shared pool join fails
    ↓
starts another independent analyzer
    ↓
memory pressure worsens further

So the current strategy would rather return exit code 75 than silently expand memory usage.

3. Idle Root Reclamation

The pool periodically samples system memory when idle. After consecutive pressure detection, it only reclaims roots that meet these conditions:

Ongoing analysis is never forcibly interrupted.

4. macOS warn vs critical

macOS memory pressure is not simply "has memory / no memory." The current strategy treats warn as a hint and critical as strong pressure.

In the multi-root pool, warn does not directly cause new roots to be rejected, nor does it repeatedly flush already-warmed roots; only critical or when the physical available margin is actually below the reserved value does it enter the multi-root pool's pressure handling.

The global single-worker budget still keeps the more conservative warn semantics, because it controls the startup of new analyzer workers.

This is a deliberate trade-off: if you only look at physical margin, the system may already be compressing memory or swapping, but the program waits until critical to start reclaiming. It is a best-effort protection, not a hard memory limit.

Why the Multi-Root Pool Is Not Enabled by Default

The multi-root pool needs to maintain analysis contexts for multiple roots and also makes memory behavior more complex.

So it is currently explicitly enabled:

set DANA_MULTI_ROOT=1

I don't want users to just upgrade the version and suddenly have the analyzer's process model change. Especially for unrelated projects, automatic discovery and automatic merging may bring unpredictable resource contention.

The explicit opt-in also has a benefit: when users are actually using multiple worktrees, they can actively choose to share; when they want full isolation, they can keep the default behavior.

Project Address and Quick Start

This post involves two separate packages, which solve problems at different layers:

Installing dana

dart pub global activate dart_analysis_daemon

After installation, you can directly check a project:

dana check --root path/to/project --files lib/main.dart

If the same repository has multiple worktrees, you can explicitly enable the multi-root pool:

# 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

Installing the IDE Analyzer Proxy

dart pub global activate dart_analysis_server_proxy

The proxy connects to the IDE through Dart-Code's dart.analyzerPath. After the IDE's analyzer warms up, dana can reuse that analysis state.

Full parameters, environment variables, and IDE configuration for both projects are in their respective READMEs:

What Scenarios Does This Fit?

I think it fits these scenarios especially well:

  1. Dart/Flutter projects with high-frequency AI modification and frequent verification;
  2. incremental checks in large repositories;
  3. multiple Git worktrees from the same repository;
  4. development flows where the IDE already has an analyzer running and the command line also needs checks;
  5. teams that want to reduce analyzer restart costs but don't want to maintain a custom SDK.

It should not be understood as:

My Final Conclusion

AI programming turns "code analysis" from a low-frequency action into a high-frequency feedback step.

If every AI modification restarts the analyzer, then waiting time and memory costs in large projects are multiplied. A more sensible direction is to manage the analyzer as a long-lived service:

start once, reuse continuously;
multiple worktrees, share on demand;
insufficient memory, clearly reject or reclaim;
don't modify the analyzer source, keep the SDK upgradable.

From the experiments so far, the most certain benefit of the multi-root pool is not "making one analysis much faster," but reducing the memory and startup overhead from repeated analyzers.

For AI programming, this is already valuable: when the verification action becomes cheap, the AI's modify-verify-fix loop is not held back by infrastructure.

Conclusion

I now prefer to view the analyzer as a service that should be reused, not a tool that is temporarily started each time a command runs.

That's the core reason I built dana: to let AI verify code more frequently, keep the feedback loop of large Dart/Flutter projects within an acceptable range, and keep the memory risk from multi-task and multi-worktree under an explainable scope.

This path is not perfect. Memory sampling has lag, multiple roots won't magically merge into one context, and macOS pressure states still need continuous observation on real devices. But at least the problem has changed from "restarting the analyzer every time" to "how to manage a long-running analyzer service."

I think that's a direction worth further optimization.

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

北苑

Nice, with a slightly weaker computer analyzing a file once is very time-consuming. I'll try it out when I have time.