In Flutter, what work runs on the UI thread versus the raster thread, and what changed when the UI and platform threads were merged?
answer
- Dart builds, the engine draws
- layer tree crosses threads
- raster thread talks to GPU
- 3.29 merged on iOS and Android
- Dart now on platform main thread
basics
~20 sThe UI thread runs Dart (build, layout, paint) and produces a layer tree; the raster thread renders it through the GPU. Since Flutter 3.29 on iOS and Android, Dart runs on the platform main thread; rasterizing stays separate.
solid answer
~50 sFlutter splits a frame between two threads. The **UI thread** runs the root isolate's Dart code, framework and app alike: event handlers, `build`, layout and `paint`, which records drawing into a **layer tree** and sends it to the engine as a `Scene`. The **raster thread** runs engine code that turns that scene into GPU commands with the renderer (Impeller on mobile and desktop). They pipeline: while frame N is rasterized, frame N+1 can be built. Historically the UI thread was separate from the **platform thread**, the host OS's main thread where native plugins and platform views run. Since Flutter 3.29 on iOS and Android (3.35 on macOS and Windows, 3.41 on Linux) the two are **merged**: Dart runs on the platform's main thread. The raster thread is unchanged. This lets Dart call main-thread-only native APIs synchronously through FFI, and it means long synchronous Dart work also stalls native event handling.
go deeper
Recall that your Dart code builds and paints a description of the frame, and a separate raster thread draws it.
Explain the layer tree handoff, why the two threads pipeline, and what the merged UI and platform thread means for where Dart runs.
Attribute a slow frame to the right side, and know the merge dates per platform so synchronous native interop and main-thread blocking are judged correctly.
Set guidance on which work may run synchronously on the main thread now that Dart shares it, and when native interop should move from channels to FFI.
## The threads a Flutter app uses A Flutter app has to cooperate with three kinds of work, and the engine gives them threads: - **Platform thread.** The host OS's main thread: the one that runs the Android `Activity` or the iOS view controller, receives OS events, and runs native plugin code and platform views. - **UI thread.** The thread that executes the **root isolate**: all of your Dart code and the Flutter framework. Since 3.29 on mobile this is the platform thread itself (see below). - **Raster thread.** Engine-owned. It executes the graphics code that draws each frame through the GPU. Your Dart code cannot run here or reach its data. ## Who does what in a frame 1. **UI side (Dart).** Handles input and timers, runs `build`, then layout and `paint`. Painting does not produce pixels: it records drawing commands into **layers**. At the end of the frame, `RenderView.compositeFrame` builds a `ui.Scene` from the **layer tree** and hands it to the engine. 2. **Raster side (engine).** Takes that scene and renders it: Impeller on iOS, on Android API 29 and newer, and on desktop in 3.47; Android below API 29 still uses the legacy Skia OpenGL ES renderer, and the web still renders with Skia. Because the handoff is a description, the two sides **pipeline**: the raster thread can draw frame N while the UI side builds frame N+1. This is why a frame has two costs. A frame can be slow because Dart work is slow (too much building or layout), or because the scene is expensive to draw even though it was cheap to describe. Reading those two bars in DevTools is covered by the performance tooling topics. | | UI thread | Raster thread | |---|---|---| | Runs | Dart: framework and app | engine graphics code | | Produces | a layer tree / `Scene` | GPU commands and pixels | | You can block it with | heavy synchronous Dart | expensive-to-draw scenes | | Reachable from Dart | yes, it is where Dart runs | no | ## Where asynchronous Dart fits `async`/`await`, `Future`s and `Stream`s do **not** move work onto another thread. They run on the same isolate's event loop, so a long synchronous loop in a stream handler blocks the frame just like a long `build`. CPU-heavy work goes to another isolate; that mechanism is covered in the Dart isolates topic. ## The thread merge Originally the UI thread was separate from the platform thread. That design had a cost: native APIs that must be called on the OS main thread could only be reached asynchronously, through platform channels, and Dart FFI could not call them directly. Flutter merged the two: | Platform | Merged by default since | |---|---| | iOS, Android | Flutter 3.29 | | macOS, Windows | Flutter 3.35 | | Linux | Flutter 3.41 | On a merged platform there is **no separate UI thread**: the root isolate's Dart code runs on the platform's main thread. The **raster thread stays separate**, so rendering still pipelines with building. Consequences worth knowing: - **Synchronous interop becomes possible.** Dart FFI can call APIs that must run on the main thread, without a channel round trip. - **Blocking Dart blocks more.** A long synchronous Dart task now also delays native event handling and plugin code on that thread. It already dropped frames before; the advice is the same: keep frame work small and move heavy computation to another isolate. - **Most apps see no difference.** The framework's migration note says merged threads should not affect your app. ## What this means when you write code - Keep each frame's Dart work well under the frame budget; on a merged platform the same budget now also covers native responsiveness. - Do not expect a platform-channel call to overlap with a long synchronous Dart loop on a merged platform: both need the same thread. - Use `await` boundaries or another isolate to break up large jobs, rather than hoping a thread will absorb them. - Treat an expensive scene (blurs, many layers) as raster-side cost that no amount of Dart optimisation removes. ## Common wrong models - "Flutter draws on the UI thread." The UI side only records; the raster thread draws. - "The merge put rendering on the main thread." Only Dart moved; rasterizing is still on its own thread. - "`async` code runs on a background thread." It runs on the same isolate's event loop.
- A screen builds quickly but still drops frames while a complex blurred card animates. Which thread is the likely bottleneck?The raster thread. The UI side only has to describe the card, which is cheap, but drawing the blur and its layers every frame is expensive GPU work. The fix is in what you ask the renderer to draw, not in reducing rebuilds. Diagnosing raster cost in detail belongs to the raster performance topic.
- After the thread merge, does an await in Dart code still let native UI events through?Yes. An `await` returns control to the event loop, so the thread can process other tasks, including native events on a merged platform. What blocks everyone is a long synchronous stretch without any await, such as parsing a huge JSON string inline; that belongs in another isolate.
saying these in an interview costs you the question
- Flutter's UI thread rasterizes the frame and talks to the GPU directly.
- The thread merge moved rendering onto the platform's main thread.
- Using async and await moves Dart work onto a background thread.
- Dart code can run on the raster thread to speed up painting.
- Merged threads apply to every platform, including the web, since 3.29.