Repository navigation
Description
Steps to reproduce
Steps to reproduce
- Use Flutter 3.47.1 stable
- Clone the minimal reproduction project: https://github.com/EgorKoschel/flutter_impeller_windows_test
- Run the application in release mode with the default Impeller renderer:
flutter run -d windows --release - Observe the startup timing output.
- In windows/runner/main.cpp, uncomment the following line to disable Impeller and use Skia:
project.set_impeller_switch(flutter::ImpellerSwitch::Disabled); - Run exactly the same command again:
flutter run -d windows --release - Compare the startup timings.
The project is a minimal Windows-only Flutter application without third-party dependencies or application-specific initialization.
With Impeller enabled, almost all of the additional startup time is spent while constructing flutter::FlutterViewController, before plugin registration.
Impeller
[STARTUP] DartProject created: 1 ms
[STARTUP] Win32Window OnCreate completed: 3 ms
[STARTUP] Before FlutterViewController: 3 ms
[IMPORTANT:flutter/shell/platform/embedder/embedder_surface_gl_impeller.cc(126)] Using the Impeller rendering backend (OpenGLESSDF).
[STARTUP] FlutterViewController created: 1186 ms
[STARTUP] Plugins registered: 1186 ms
[STARTUP] Flutter view attached: 1190 ms
[STARTUP] Window/Create completed: 1190 ms
[STARTUP] First Flutter frame: 1232 ms
Skia
[STARTUP] DartProject created: 1 ms
[STARTUP] Win32Window OnCreate completed: 4 ms
[STARTUP] Before FlutterViewController: 4 ms
[STARTUP] FlutterViewController created: 58 ms
[STARTUP] Plugins registered: 58 ms
[STARTUP] Flutter view attached: 69 ms
[STARTUP] Window/Create completed: 69 ms
[STARTUP] First Flutter frame: 165 ms
The FlutterViewController constructor therefore takes approximately:
- Impeller: 1183 ms
- Skia: 54 ms
The time to the first Flutter frame is approximately:
- Impeller: 1232 ms
- Skia: 165 ms
I repeated the test multiple times with both renderers. The results were consistent across repeated launches: Impeller consistently spent roughly 1 second or more constructing FlutterViewController, while Skia completed the same step in only a few dozen milliseconds. The large difference was reproducible and was not limited to the first launch.
Main channel
The issue is also reproducible on the current Flutter main channel, although the performance characteristics are different from Flutter 3.47.1 stable.
Impeller
[STARTUP] DartProject created: 1 ms
[STARTUP] Win32Window OnCreate completed: 3 ms
[STARTUP] Before FlutterViewController: 3 ms
[IMPORTANT:flutter/shell/platform/embedder/embedder_surface_gl_impeller.cc(127)] Using the Impeller rendering backend (OpenGLESSDF).
[STARTUP] FlutterViewController created: 101 ms
[STARTUP] Plugins registered: 101 ms
[STARTUP] Flutter view attached: 113 ms
[STARTUP] Window/Create completed: 113 ms
[STARTUP] First Flutter frame: 618 ms
Skia
[STARTUP] DartProject created: 1 ms
[STARTUP] Win32Window OnCreate completed: 3 ms
[STARTUP] Before FlutterViewController: 3 ms
[STARTUP] FlutterViewController created: 47 ms
[STARTUP] Plugins registered: 47 ms
[STARTUP] Flutter view attached: 55 ms
[STARTUP] Window/Create completed: 55 ms
[STARTUP] First Flutter frame: 134 ms
On the main channel, FlutterViewController creation is significantly faster than on Flutter 3.47.1 stable. However, startup with Impeller is still substantially slower overall.
Time to first Flutter frame:
- Impeller: 618 ms
- Skia: 134 ms
Impeller therefore takes approximately 4.6x longer to reach the first Flutter frame in this run.
On Flutter 3.47.1 stable, most of the delay occurs while constructing FlutterViewController. On the current main channel, that part has improved substantially, but a significant startup delay remains after FlutterViewController has been created and before the first Flutter frame is rendered.
Code sample
Minimal reproduction:
The relevant timing instrumentation is placed immediately around the FlutterViewController constructor:
PrintStartupTime("Before FlutterViewController");
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left,
frame.bottom - frame.top,
project_);
PrintStartupTime("FlutterViewController created");
Impeller is enabled by default.
For comparison with Skia, the reproduction project contains the following commented-out line in windows/runner/main.cpp:
flutter::DartProject project(L"data");
// Uncomment to disable Impeller and use Skia.
// project.set_impeller_switch(flutter::ImpellerSwitch::Disabled);
No other code is changed between the Impeller and Skia measurements.
Performance profiling on master channel
- The issue still persists on the master channel
Timeline Traces
The observed delay occurs during native Windows engine initialization, inside the flutter::FlutterViewController constructor and before plugin registration or the first Flutter frame.
The C++ timing instrumentation in the reproduction project was used to isolate this native startup delay.
Video demonstration
What target platforms are you seeing this bug on?
Windows
OS/Browser name and version | Device information
Windows 11 Pro 25H2, x64
OS build: 26200
GPU: Intel(R) UHD Graphics 770
GPU driver version: 32.0.101.7088
GPU driver date: 2026-06-17
Does the problem occur on emulator/simulator as well as on physical devices?
Unknown
Is the problem only reproducible with Impeller?
Yes
Logs
Flutter Doctor output
[√] Flutter (Channel stable, 3.47.1, on Microsoft Windows [Version 10.0.26200.9168], locale de-DE) [233ms]
• Flutter version 3.47.1 on channel stable at C:\src\flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 6655482 (8 days ago), 2026-08-19 10:07:23 -0700
• Engine revision 5d53178
• Dart version 3.13.1
• DevTools version 2.60.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[√] Windows Version (11 Pro 64-bit, 25H2, 2009) [1.253ms]
[√] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1.466ms]
• Android SDK at C:\Users\EgorKoschel\AppData\Local\Android\sdk
• Emulator version 35.4.9.0 (build_id 13025442) (CL:N/A)
• Platform android-36, build-tools 36.0.0
• Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: flutter config --jdk-dir="path/to/jdk".
• Java version OpenJDK Runtime Environment (build 17.0.6+0-b2043.56-9586694)
• All Android licenses accepted.
[√] Chrome - develop for the web [60ms]
• Chrome at C:\Users\EgorKoschel\AppData\Local\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.6.5) [59ms]
• Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
• Visual Studio Community 2022 version 17.6.33829.357
• Windows 10 SDK version 10.0.22000.0
[√] Connected device (3 available) [139ms]
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.26200.9168]
• Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.174
• Edge (web) • edge • web-javascript • Microsoft Edge 151.0.4129.107
[√] Network resources [362ms]
• All expected network resources are available.
• No issues found!
Activity
puneetkukreja98 commentedon Aug 27, 2026
@EgorKoschel Thanks for reporting this, routing this to team-engine for further investigation.
Findings:
Impeller:
Impeller-disabled:
Flutter doctor -v:
krll-kov commentedon Aug 27, 2026
Same for me
fix: Impeller 开关一直是空操作;改用 set_impeller_switch,CI 出双渲染器包
SugarFatFree commentedon Aug 29, 2026
Confirming this on a different machine, with a real application rather than a minimal repro: MarkText Plus, a Flutter desktop Markdown editor (window_manager, file_picker, printing, etc.).
Time from process start to first frame, from the app's own startup trace, Windows release builds:
set_impeller_switch(ImpellerSwitch::Disabled))Two things this adds to the thread:
The 3.44.9 baseline. Same machine, same app, a version predating Impeller-by-default on Windows. It lands on the same numbers as 3.47.2-with-Skia, which rules out a general 3.47 startup regression — the cost tracks the renderer, not the version.
A trap for anyone else trying to A/B this. My first attempt switched renderers with the
FLUTTER_ENGINE_SWITCHES/FLUTTER_ENGINE_SWITCH_nenvironment variables. In a release build those are ignored:GetSwitchesFromEnvironmentis compiled out behind#ifndef FLUTTER_RELEASE, so both arms of the experiment run Impeller and the measured difference is noise. I concluded from that run that Impeller was not the cause, and was wrong.project.set_impeller_switch(...)— as used in the report above — is the only thing that works in a release build, and it reproduces the difference immediately.The machine is a virtualised GPU on a corporate Windows host, which likely explains why the absolute numbers here are worse than the 1232 ms / 165 ms in the original report while the ratio is comparable.
gaaclarke commentedon Aug 31, 2026
A certain amount of this is expected because we are compiling shaders at launch to avoid jank while the app is running. The opengles backend doesn't let us precompile shaders like we can for metal and vulkan. Still this is a really high number that we can try to investigate. Ultimately we might have to just rely on a vulkan/dx12 backend though.
fix(windows): temporarily disable impeller startup regression
tocreator commentedon Sep 6, 2026
This isn't just about the startup time increasing to 3 or 4 seconds; the overall runtime performance in production has also degraded by roughly 20% to 35%.
krll-kov commentedon Sep 17, 2026
I measured startup on real hardware and tried two changes in a local engine build. I'm sharing the numbers and have two questions: are there known problems with a GL program cache on Windows, and is there a reason Impeller does not rely on
GL_KHR_parallel_shader_compile?How it was measured
e619960004d(master) and from 3.47.4. The app was built with the matching SDK.FragmentProgramtiles. It reads the process creation time withGetProcessTimesand prints how long it took to reachmainand untilwaitUntilFirstFrameRasterizedcompletes.BW_SHADERS=0runs the same app without shaders.windows/runner/main.cpppicks the renderer from an environment variable, so both renderers run the same build:lib/main.dart
pubspec.yaml (flutter section) and the 8 shaders
shaders/raymarch.fragshaders/clouds.fragshaders/voronoi.fragshaders/mandelbrot.fragshaders/julia.fragshaders/tunnel.fragshaders/starfield.fragshaders/caustics.frag3.47.4, median of 5 runs
mainA build with timestamps shows that Impeller links 80 GL programs in this app, 2.76 s in total. On 3.47.4 most of them are linked while
ContentContextis constructed, before the root isolate starts:CreateDefaultasks for async pipelines, butReactorGLES::AddOperationreacts at once on the thread that holds the context.master
e619960004d, median of 3 runsmainThe local build also has timestamp logging and some unrelated local patches, and without the experiments it was 15% faster than the official build; I did not look into why. With #185723
mainstarts as early as with Skia. The first frame still waits for the programs, which the IO thread now links one at a time: 79 programs, 2.97 s in total.Experiment 1: ANGLE program cache
The Windows embedder calls
eglSetBlobCacheFuncsANDROIDaftereglInitialize. ANGLE then stores its compiled programs as files, here in%LOCALAPPDATA%\FlutterAngleCache\<hash of the exe path>. Nothing else changes.mainSkia made no calls to the cache in these runs; I did not look into why.
Diff against e619960
Experiment 2: parallel link
ANGLE on D3D11 reports
GL_KHR_parallel_shader_compile, and its default thread count is the maximum.LinkPrograminpipeline_library_gles.ccqueries the compile and link status right after each call, so each program waits for the previous one. In the experiment the IO thread first callsglCompileShaderandglLinkProgramfor every queued pipeline, then queries the results in separate tasks.glDetachShaderandglDeleteShadermove after the status query, since ANGLE waited for the link on those calls.mainThe first launch is about 30% faster.
mainstarts 70-150 ms later because the batch occupies the IO thread. The 8FragmentProgramshaders do not go through the compile queue, so they still link one at a time (1.02 s).Diff against e619960
Questions
fml::paths::GetCachesDirectory()is not implemented on Windows, and Add embedder API to enable the Vulkan pipeline cache #186374 asks for a similar setting for the Vulkan pipeline cache.GL_KHR_parallel_shader_compilein the GLES backend, for example driver problems on Android?gaaclarke commentedon Sep 17, 2026
Thanks @krll-kov both of those sound very promising.
First, there is no technical reason we can't use these techniques. As to why they haven't been used yet, it's a bit of an interesting story that I'll share. The history of the OpenGLES Impeller renderer started on Android where
GL_KHR_parallel_shader_compilewouldn't have even been available. The OpenGLES renderer on Android was a legacy renderer, where the future was in Vulkan. That means an hour spent on Vulkan optimization was more valuable than an hour spent on OpenGLES optimization.The calculus changed when we shipped for Windows though, OpenGLES has been promoted to a first tier renderer. So, we are playing catch up on performance there. We have recently explored promoting Vulkan on Windows, but that's probably not happening in the foreseeable future.
FWIW Vulkan's startup times are not as bad to push us to look into the cache yet. ANGLE is particularly bad for startup time because the shaders are in a higher level format and they need to be cross compiled to HLSL by ANGLE. So there is a lot of potential value there.
Here is the benchmark for windows startup time, if you want to see officially what we use to measure it: https://flutter-flutter-perf.luci.app/e/?begin=1789574716&enable_chart_tooltip=true&end=1789661116&plotSummary=true&queries=test%3Dflutter_gallery_win_desktop__start_up&use_titles=true
krll-kov commentedon Sep 17, 2026
Thanks for the benchmark! I've tried to go further with the GL_KHR_parallel_shader_compile approach, which I like better for reasons explained beneath and here are my results with reworked code: Not only it became faster, it also removed significant junk on the first frame with example from my previous message, where multiple shaders are rendered on screen
The change stays in the GLES backend.
PipelineLibraryGLESsplits a program link in two: a compile queue job starts the shaders andglLinkProgramwithout querying any status, and a later job on the same task runner checks the statuses. It runs only when the driver reportsGL_KHR_parallel_shader_compileandDescriptionGLES::IsANGLE(), only for async requests, and only inside a compile queue job. Everything else, including the synchronous path and any other driver, keeps the current sequence of GL calls.Full diff, 6 files, +651 −33
Two things turned out to matter more than the parallelism itself:
GL_COMPLETION_STATUS_KHRin a loop keeps the IO thread busy and delays the root isolate. On flutter_gallery that cost 94-104 ms oftimeToFrameworkInitagainst 58 ms before the change. Letting the compile queue drain and then checking the statuses in one job removed that.std::clamp(hardware_concurrency / 2, 1, 4), the same bound asContextVK::ChooseThreadCountForWorkersafter [Impeller] Concurrent VkPipeline creation is slow on Mokey Device. #143540, broughttimeToFrameworkInitback to 62-64 ms. Batches of 2 were measurably worse for the first frame (606 ms against 455 ms), batches of 8 no better.Measurements, one build in every row,
oldbeing the same binary with the new path disabled:flutter_gallery, profile,
start_up_info.json, 5 rounds with a warmup, median:flutter_gallery on a Ryzen AI 9 HX 370 with a Radeon 890M, time from process creation until the runner shows the window, 3 rounds, median:
An app with 8
FragmentProgramshaders, from process creation to the first rasterized frame, same machine: 2530 ms with the official engine, 2587 ms with the path disabled, 1826 ms with it on.The startup jank is the part I did not expect to move. On the same machine the second after the first frame runs at 113-114 FPS with 3.5 ms of raster on the new path, against 72-78 FPS and 7.6-8.6 ms before. From the second second on both are at 120 FPS.
Also checked: flutter_gallery and 8 scenes of a local bench (fragment shader,
BlendMode.multiply,saveLayer,BackdropFilter,ImageFiltered,ColorFiltered,drawRawAtlas, text) produce pixel identical screenshots on Skia and Impeller with the animation frozen; closing the window while programs are still linking exits with code 0 in 20 runs out of 20 and is faster than before (166 ms against 370 ms);clang-formatand the engine clang-tidy are clean andimpeller_unittestsshows no regressions.Questions before I send it:
pipeline_library_gles.ccplus a small addition toPipelineCompileQueueGLES, and it adds a pending list, a finish job and the cap.I dropped the program cache for now. Correctness is not the problem: ANGLE keys entries with
ANGLE_PROGRAM_VERSION, rejects a binary whose adapter id or feature level does not match, and the stored D3D bytecode survives driver updates. What stopped me is everything around it.fml::paths::GetCachesDirectory()is not implemented on Windows, so the embedder has to pick a folder, apps will want to move it or turn it off, which means a new embedder setting similar to what #186374 asks for Vulkan, and nothing would remove the entries that every Flutter update with a new ANGLE leaves behind. That is a bigger design discussion than a startup fix, and the parallel compilation gets most of the win without keeping any state on disk. If you would rather have the cache first, I can put the numbers for it with a warm cache the first frame on this app was 328 ms.0nilinkz commentedon Sep 28, 2026
Following up on @krll-kov's ANGLE program cache experiment with numbers from a
second machine (Ryzen 7 2700X, GTX 1080, driver 32.0.15.8266, Windows 11), and
a proposal for the three questions that stopped it: where the files go, how
apps move or turn it off, and what removes old entries.
Where the time goes on 3.47.2. In a profile build of a real app, engine
start to
main()is ~1.7 s: 72impeller::LinkProgramcalls on the rasterthread before
main(). A forwardingD3DCompiler_47.dllin a minimal appcounts 146
D3DCompilecalls (1.93–1.94 s of CPU; ANGLE compiles eachvertex/pixel pair concurrently, so 1.50 s on the critical path). Every launch
pays it again.
With the blob cache (3.47.2 engine, local profile build, same app,
alternating fresh launches, three each):
main()main()D3DCompilecalls(
D3DCompilecounts from the minimal app, whose extra program adds 2 calls.)On
main(2abcfdc6), where #185723 moved program setup to the IOthread,
main()starts within 50 ms but the first frame still waits for abouthalf of the compiles:
main2abcfdc6(Three alternating launches each; the app holds its first frame for one extra
program of its own.
--trace-startupstops at the first frame, which onmaincomes after 34–43 of the 72 links; the rest link on the IO thread afterwards.)
Proposal for the open questions (implemented and unit-tested locally):
path_providerdoeson Windows:
%LOCALAPPDATA%\<CompanyName>\<ProductName>\flutter_program_cachefrom the executable's version resource, falling back to the executable name.
Per user, stable across installs and updates, never shared between users.
FlutterDesktopEngineProperties::program_cache_pathandDartProject::set_program_cache_path(): unset uses the default, empty turnsthe cache off. In debug and profile builds a
--program-cache-path=<folder>engine switch overrides it, for tests thatneed a cold cache.
the first write of a new version removes other versions' folders (only files
the cache itself writes, and a folder only once empty). The total is bounded
(32 MiB, least recently used first), and a single entry to 4 MiB.
Correctness stays with ANGLE: its key covers the shader sources, ANGLE's
program format version,
GL_RENDERER(GPU, device id and driver version) andbindings, and it rejects a binary whose adapter or feature level differs; a
rejected or undecodable entry is relinked and replaced. The store adds a
checksum, writes through a temporary file and an atomic rename, reads the file
once per hit, and writes on a background thread so a warm launch starts no
thread.
This is complementary to #193066: parallel links shorten the first launch,
the cache removes the compile from later launches. I've opened #193419 with
this change as a Windows embedder change, and I'm happy to adjust the API
shape (default-on vs opt-in, naming) to what you prefer. @krll-kov, if you'd rather
send your version, I can contribute these tests and numbers to it instead.
[Windows] Keep ANGLE's compiled GL programs across launches
krll-kov commentedon Sep 28, 2026
Thanks, my code was just a draft to demonstrate the time change and i was not sure because of many opened questions for potential issues so I decided to do parallel linking instead
Disable Impeller on Windows to restore startup time
scalz commentedon Oct 14, 2026
Adding a data point from Flutter GPU, where the same cause (no persisted programs on the OpenGL ES backend) costs far more than at startup, and where the two fixes in progress would not help equally.
What is different from the startup case. The programs here are not the engine's own: they are the ones a package builds through Flutter GPU (
flutter_scene0.24.3), compiled at run time when a feature is first drawn. Moving the engine's program setup to the IO thread (#185723) does not touch them. A persisted ANGLE blob cache (#193419) sits below Impeller, so it should; I have not tried a build with it.Numbers. Release builds, Ryzen 5 2600, Radeon RX 580 (driver 31.0.21923.11000), Windows 11 25H2. The sample below builds a scene of ten primitives with the standard lit material and calls
Scene.warmUpthree times, each time with one more feature, then once more with the last configuration. Each step only pays for what the previous ones did not compile. Three consecutive launches of the same build:Stable 3.47.6 (engine 692136c):
mainchannel, 3.49.0-1.0.pre-418:Within a process the pipelines are cached (step 4), so the cost is the compile, and every new process pays it again: about 2 s for an application that only uses lit materials, 8 s with shadows, 20 to 24 s with ambient occlusion as well. The application is frozen during each step (the progress bar of the sample stops): on OpenGL ES,
lib/gpu/render_pass.cchas the UI thread wait for a raster task that requests the pipeline and waits for its link. The same figures were first seen in a real application (a 3D floor plan), which I cannot share, hence the sample.Steps 1 to 3 build 3, 2 and 4 pipelines, and two programs carry most of it: the lit fragment shader with shadows links in 6 s, and its full variant (used once ambient occlusion is on) in 14 s, measured alone on ANGLE with the same sources. These are large physically based shaders, and part of the time comes from two patterns that
D3DCompilehandles slowly; a change on theflutter_sceneside (bdero/flutter_scene#502) takes the total from 23.6 s to 9.5 s on itsmaster, with identical frames. The remaining 9.5 s, for nine pipelines, is still paid at every launch, and that is the part only a persisted cache removes.Why it matters for the two changes in progress.
lib/gpu/render_pass.ccrequests its pipeline as async and waits on the future directly. That is the path measured here. From reading [impeller] parallelize opengles shader compiles #194022, the new queue finishes such a job on its own, so nothing hangs, but I expect no gain either: Flutter GPU requests one pipeline and blocks until it is linked, so there is never more than one link in flight.Reproduction. https://github.com/scalz/issue_windows_impeller_no_pipeline_cache, which is a new Windows project with
flutter_scene: 0.24.3andvector_math, one line added inwindows/runner/main.cpp:then
flutter run -d windows --release. Close and launch again: steps 1 to 3 cost the same. Each launch appends its timings to%TEMP%\pipeline_cache_repro.log.lib/main.dart
Console output of one launch (stable 3.47.6)
[impeller] parallelize opengles shader compiles (flutter#194022)
fix(windows): 关闭 Impeller 渲染后端,修掉启动要等 2~4 秒才出界面
[impeller] parallelize opengles shader compiles (flutter#194022)