Repository navigation
Description
Steps to reproduce
Steps to reproduce
- Take a large Flutter web app. Ours is code-generated: 10,601 Dart files, 128 MB of source, which compiles to 1936 DDC modules.
flutter run -d web-server(debug).- Wait for the compile to finish, then open the served URL.
Same machine (Windows 11, 32 GB RAM, 11+ GB free), same project, runs isolated - nothing else building or serving.
Expected results
Debug start-up cost comparable to 3.24.5, and the module loader reaching the end on an app of this size.
Notes
- Release is unaffected: build time +10 %, bundle size +1.4 %. The regression is confined to the DDC/debug path, which is what makes it a developer-experience problem rather than a shipping one.
- The module count barely depends on app size - a 3x smaller generated app (3,558 files) still declares 1731 modules, because the count is dominated by the framework and packages rather than app code. Splitting the app is therefore not a workaround.
--no-web-experimental-hot-reload(deprecated) switches back to the require.js loader. It compiled faster in the one measurement we took, but that run was not isolated so we are not quoting a figure; its loader is serial, and the load phase did not improve.- 3.47.4 compiles the same sources with no code change (clean
pub get, zero errors), so this is not a migration artifact on our side. It does not fix the problem either: the loader stops at ~1600 of 1933 modules and the app stays on its splash screen, with the dev server still CPU-bound at ~1.4 cores and 12 connections open. A fresh request issued by hand at that moment is answered in 0.7 s, so the server is not deadlocked - the browser simply stops making progress. - Workaround in use:
--profile, which builds with dart2js and runs fine, at the cost of hot reload and ~9 min per cycle.
Actual results
| 3.24.5 | 3.44.9 | 3.47.4 | |
|---|---|---|---|
| debug compile (cold) | 176 s | 685 s | 547 s |
| time to first frame | 149 s | never reached | never reached |
release build (flutter build web) |
316 s | 347 s | - |
release main.dart.js |
39.1 MiB | 39.6 MiB | - |
On 3.44.9 the app never starts: the browser loads modules for several minutes, then the console shows
DDC is about to load 1936/1936 scripts with pool size = 1000
Failed to load DDC scripts after 6 tries
and nothing further is requested. The splash screen stays up indefinitely. Waiting for connection from debug service never completes.
While the browser is loading, the dev server (dartvm) is CPU-bound at ~1.4 cores and serves one 1.4 KB module in ~0.9-1.1 s. Even a 404 takes ~0.6 s. Under the same load, 3.24.5 serves a comparable module in ~0.44 s, and ~2 ms when idle.
Relevant constants in dart-sdk/lib/dev_compiler/ddc/ddc_module_loader.js:
this.maxRequestPoolSize = 1000;
// Max retry to prevent from load failing scripts forever.
this.maxAttempts = 6;Code sample
Code sample
[Paste your code here]Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[Paste your output here]
Activity
bdemeure commentedon Sep 12, 2026
[√] Flutter (Channel stable, 3.44.9, on Microsoft Windows [version 10.0.26200.9445], locale fr-FR) [559ms]
• Flutter version 3.44.9 on channel stable at C:\dev\flutter.3.44.9
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 6b182d2 (5 weeks ago), 2026-08-05 10:04:07 -0700
• Engine revision 5a2a6a4
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop,
enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager,
omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[√] Windows Version (Windows 11 or higher, 25H2, 2009) [1 653ms]
[√] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [3,8s]
• Platform android-36, build-tools 35.0.0
• Java version OpenJDK Runtime Environment (build 25.0.2+-15348964-b329.117)
• All Android licenses accepted.
[√] Chrome - develop for the web [213ms]
• Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop Windows apps (Visual Studio Community 2026 18.9.2) [212ms]
• Windows 10 SDK version 10.0.26100.0
[√] Connected device (3 available) [501ms]
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [version 10.0.26200.9445]
• Chrome (web) • chrome • web-javascript • Google Chrome 152.0.7977.83
• Edge (web) • edge • web-javascript • Microsoft Edge 152.0.4191.66
[√] Network resources [813ms]
• All expected network resources are available.
• No issues found!
mbcorona commentedon Sep 14, 2026
Hi @bdemeure, thanks for the detailed report.
This looks like the new DDC module loader that replaced require.js scaling badly to ~2000 modules, so debug web never reaches first frame. Routing to the web team for further investigation
bdemeure commentedon Sep 18, 2026
Thanks @mbcorona. We went digging, and the module-count hypothesis does not hold.
The app is not asking for 1936 small modules - it is asking for one module of
590 MB, and that is what never loads.
1. The module count is not what breaks it
Synthetic Flutter web apps, same machine, same SDK (3.44.9 - 3.47.4 produces
byte-identical modules, see section 5, so none of this is fixed on latest):
At 5x the module count under suspicion, with
pool size = 1000, start-up is fine.Neither module count, nor source volume, nor the two together reproduce anything.
2. What our app actually serves
The dev server declares 1936 modules. 541 of them belong to the app package, and
540 of those have a median size of 22.6 KB. The last one does not:
That single request is the whole problem. Every other module queues behind it,
which is why 1.4 KB files take ~1 s, why the loader gives up with
Failed to load DDC scripts after 6 tries, and why first frame never arrives.3. Where the 590 MB comes from
Walking the import graph from
main.dart, the app package has 10 414 reachablelibraries - but only 541 exist as modules. The other 9 873 are absent from the
module list, and the ones we sampled return 404.
They are not missing, they are merged. 9 872 libraries of the app package form a
single import cycle: each of them imports the next, directly or through a chain,
and the chain closes back on itself. Exactly one member of that cycle appears in
the module list -
startup/view/startup_screen.dart, the library the 590 MBmodule is named after. The other 9 871 have no module of their own.
main.dartis inside that cycle too, which is why it has no module either.
So DDC puts every library of an import cycle into one module. Our generated code
happens to form one very large cycle, so we get one very large module.
4. Reproducing it without our app
From an empty Flutter app, this is enough:
nocycleas a third argument generates the very same libraries without the importring - same count, same volume - so the cycle is the only variable between two runs.
The script writes
lib/gen/g0.dart...lib/gen/gN-1.dart, where each libraryimports the next and the last imports the first, plus a
lib/gen_all.dartthatreaches the cycle, and it patches
lib/main.dartto call into it (without thatlast step nothing is reachable, so nothing is compiled).
Results, reading the module list from
main_module.bootstrap.js:make_cycle.py 2000 100 nocyclemake_cycle.py 2000 0make_cycle.py 2000 100make_cycle.py 2000 140The first two rows are the point: same 2 000 libraries, same 165 MB of source, and
removing the import ring turns one 456 MB module into 2 000 modules of 227 KB - and
a 4-minute start-up into a 20-second one.
The last row is our own situation reproduced without any of our code: a 618 MB module,
a browser tab that stops rendering, and no first frame after 20 minutes of waiting.
The app compiled in 759 s, which is the same order as the 685 s we report above.
5. What changed between 3.24.5 and 3.44.9, and why 3.47.4 does not fix it
Merging is not new: 3.24.5 merges the same cycle into the same single module.
What changed is how big that module is. Same 2 000-library cycle, same source:
main.dart)Ordinary code grew 31%; the merged module grew 225%. 3.47.4 emits exactly the same
5 498 243 bytes as 3.44.9 for that module - byte-for-byte identical - so whatever
landed since 3.44.9 has not touched this.
Two things would help us: emitting a module per library rather than per cycle,
or at least keeping the merged module's size in line with the rest.
mdebbar commentedon Sep 23, 2026
This sounds similar to #190436. @bdemeure do you have a lot of mixins in the codebase?
bdemeure commentedon Sep 24, 2026
Thanks @mdebbar. The generated app does use mixins, but only in a shallow way, and they are not needed to trigger this.
The repro has none. make_cycle.py from my previous comment generates libraries that each hold a single plain class: no mixin, no with, no inheritance, no generics. The only variable between the fast and the slow run is the import cycle: make_cycle.py 2000 100 nocycle gives 2 000 modules and first frame in < 20 s, while make_cycle.py 2000 100 (same libraries, same code, plus the import ring) gives one 456 MB module and a ~4 min start-up.
The real app, for completeness. About 1 550 of its 10 601 libraries apply a mixin, but always a single one (3 classes apply two). Almost all of them apply one of five non-generic mixins, there is no on clause and no mixin chain - nothing like the 20+ generic mixins per controller in flutter/flutter#190436.
So this looks like a different root cause from flutter/flutter#190436 - unless that ERP code also forms large import cycles, which would be worth checking there.
Markzipan commentedon Sep 28, 2026
@bdemeure Can you try to rerun your app with
flutter run -d web-server --extra-front-end-options=--no-js-strongly-connected-components? I added a hidden flag that disables SCCs since DDC's new module system doesn't require them, but they're not enabled by default. My local experiments show that this is promising, but I can send out a change to set this by default shortly (while investigating other sources of the slowdown).bdemeure commentedon Sep 29, 2026
Thanks for the pointer. I ran the measurements with
--extra-front-end-options=--no-js-strongly-connected-components.Short version: the flag fixes it.
Our real app goes from never reaching a first frame to 32.9 s, and debug compile time drops by roughly 60 %.
Setup: Flutter 3.44.9 stable, Windows 11, 32 GB RAM. Every run uses
flutter run -d web-server, one at a time, from a cold compile. "Compile" isflutter runto "is being served at". "First frame" is measured in the browser,from navigation start to the
flutter-first-frameevent.I checked that the flag really reaches the frontend server by reading the
frontend_server_aot.dart.snapshotcommand line:... --verbosity=error --no-js-strongly-connected-components --dartdevc-canary --dartdevc-module-format=ddc
Synthetic repro
Using the
make_cycle.pyfrom my earlier comment: N libraries in a single import cycle, with a given amount of filler code each.A and B share the same generated sources. I did not regenerate between them, so the flag is the only variable.
Real app
A generated Flutter app, 10,601 Dart files.
startup_screen.dart.lib.jsNotes
The flag does not change how much JS is emitted, only how it is split. For the same sources, run A emits 462,261,572 bytes of app JS and run B emits 461,349,197 bytes, a 0.2 % difference.
Run C is the one I found most informative. It has 616 MiB of app JS, more than the real app's single merged module, and it still starts in 14.9 s once that volume is spread over 2002 modules. So the total volume is not what breaks startup. A single merged module is.
Without the flag, fetching the one merged module took 439 s on its own in run A, which accounts for almost all of the 515.7 s first frame.
The flag also splits cycles inside the framework, not just in app code: the non-app module count goes from 309 to 732 in the synthetic case.
Hot reload works with the flag, both on the repro and on the real app.
No console errors in any run with the flag. The
Failed to load DDC scripts after 6 trieserror we used to get is gone.The "without flag" column for the real app comes from an earlier run on the same machine and the same project.
Set --no-js-strongly-connected-components for DDC compiles by default (…
[ddc] Optimize generated JS scans in DDC.
Set --no-js-strongly-connected-components for DDC compiles by default (…