How come Flutter is not multi-threaded

Shouman B. Shuvo

9 May, 2025

How come Flutter is not multi-threaded?

I once had a friend who was a master of the three-way text. He’d juggle conversations with three girlfriends at once. I still don’t know how he pulled it off, but he replied so fast that everyone thought he was talking to only them. To this day, I use it as a real-world example of concurrency.

Programming often mirrors real-world events. Concurrency is about managing multiple tasks simultaneously. In programming, this translates to an environment where multiple processes share a single core, with the system rapidly switching between tasks to create the illusion of simultaneous execution.

Parallelism is the simultaneous execution of multiple tasks. It’s like having more friends like him: you can do more things at once 😉. Parallelism is possible in a system with multiple cores or processors. In today’s world, almost every smartphone is powered by multi-core processors. In fact, I haven’t seen a single-core processor in quite a while.

So, the question arises: in this multicore world, why did Flutter decide to be single-threaded?

Every Dart code in Flutter runs in an environment called an “isolate.” Each isolate has its own single-threaded event loop, which handles all the events in the app. These events include user interactions, I/O operations, repaint requests, and more. They’re all lined up in a queue, waiting to be processed by the event loop.

Image source: Medium

A crucial aspect of this event loop is handling repaint events, which occur 60 times per second on a 60Hz display. If these events aren’t processed promptly, the app may appear choppy. Flutter excels at achieving a smooth user experience. However, challenges can arise when the application encounters long-running operations, such as file loading or image processing. If these processes are performed synchronously, they can freeze the UI and make it unresponsive.

Flutter’s solution to this problem is the implementation of asynchronous operations. This is achieved through the Async Trio: Future, async, and await. However, consider an operation that takes 20 seconds when run independently but 30 seconds in the main isolate due to other concurrent operations. Is there any optimization we can apply here?

Flutter expanded the horizon with isolates. As mentioned, all Dart code runs on the main isolate, which includes the main thread. Isolates are like independent workers, but they cannot access each other’s memory. Each isolate has its own heap, making them similar to processes, but they’re not. Isolates communicate with each other through messaging, preventing data races and other concurrency issues by avoiding direct memory manipulation.

So, you might be wondering if these isolates run on different CPU cores. While it’s not guaranteed, isolates can be scheduled to run on different cores depending on the availability of processor cores. All the isolates are run under the same process.

Why are Isolates not thread?

An isolate is a thread-like entity. Unlike threads, isolates do not share memory. As we discussed, the main isolate has its own event loop, and this is true for other isolates as well. To send and receive processed data, you need to use ports for communication.

Isolates are similar to TCP sockets in that they require a handshake mechanism for communication. This is because isolates don’t share memory and rely on ports and messages to pass data. A handshake ensures that both isolates are ready to communicate and can establish a reliable connection.

Just like the heap, isolates don’t share the same stack. So, how do isolates execute code in another isolate? Let’s examine the spawn method:

				
					external static Future<Isolate> spawn<T>(
    void entryPoint(T message),
    T message,
    {
        bool paused: false,
        bool errorsAreFatal,
        SendPort onExit,
        SendPort onError,
        @Since("2.3") String debugName,
    },
);

				
			

The isolate’s entry point function (the one passed to Isolate.spawn) is serialized and sent to the new isolate as a message. This message contains the function’s code and any necessary arguments. (Yay! 😀)

When sending a message between isolates, the message size is effectively doubled due to memory overhead. This is because isolates don’t share memory, so two copies of the message are created: one in the sending isolate and one in the receiving isolate. For example, sending a 1 GB file between isolates would require 2 GB of memory on the device. This is similar to an API call within the same device. One isolate cannot directly access the data of another isolate due to their isolation.
You may consider Isolates in Dart as tasks within the Dart VM, each executing independently.

Shouman B. Shuvo

9 May, 2025