How Flutter Actually Paints 🎨 Your Screen — A Deep Dive for Every Developer
What really happens between you writing Text("Hello") and a pixel glowing on a screen.
How Flutter Actually Paints 🎨 Your Screen — A Deep Dive for Every Developer
What really happens between you writing Text("Hello") and a pixel glowing on a screen.

flutter architecture
Imagine you’ve been using a car your whole life but never looked under the hood. You know pressing the accelerator makes you go faster — but why? What’s actually happening? That’s what we’re doing today, except the car is Flutter, and the engine is a surprisingly elegant piece of engineering.
The Big Picture: Flutter is a Layer Cake
Flutter isn’t one thing. It’s a carefully stacked system of four layers, each one talking to the layer below it. Look at the diagram above — every layer has a very specific job, and none of them do each other’s work. That separation is intentional, and it’s part of what makes Flutter so powerful.
Let’s walk through each layer, from top to bottom.
Layer 1: The Framework — Your World (Dart)
This is where you live as a developer. When you write Flutter code, you’re in the Framework layer.

Here’s the secret that most Flutter devs don’t know: the Framework layer secretly runs three separate trees simultaneously, all the time.
The Widget tree — This is what you write. Every Column, Text, Container — Those are widgets. The crucial thing: widgets are immutable (unchangeable). When something changes in your app, Flutter doesn't modify the widget — it throws the old one away and creates a brand new one. Widgets are cheap, disposable blueprints. Think of them like a recipe card. The recipe doesn't cook the food; it just describes what to make.
The Element tree — This is Flutter’s internal “manager.” Elements persist across rebuilds. When you call setState(), Flutter rebuilds the widget tree, but the element tree compares old and new widgets to figure out what actually changed. Elements hold the state. This is why your AnimationController survives a rebuild — the element remembers it. Think of elements like a project manager reading updated blueprints: "Okay, the kitchen changed, but the bedroom is the same — I'll only send workers to the kitchen."
The RenderObject tree — This is where the actual visual math happens. RenderObjects calculate sizes, positions, and paint themselves onto a canvas. They don’t care about your Dart widgets at all. They just answer two questions: “How big am I?” and “What do I look like?” These are the painters on the job site, following the manager’s instructions.
Why three trees? Because separating description (widget), management (element), and painting (RenderObject) keeps each concern clean and makes performance possible. If every rebuild re-calculated every pixel, your app would crawl.
Layer 2: The Engine — The Actual Drawing Machine (C++)
This is where Flutter gets really interesting, and really different from every other cross-platform framework.

The engine is written in C++ — a much faster, lower-level language than Dart — and it runs the actual frame rendering pipeline. Every frame your screen shows goes through these five phases:
Build — The Dart side runs, rebuilding any widgets that need updating. The element tree reconciles differences.
Layout — Every RenderObject gets asked: “Given these constraints (max/min width and height), how big do you want to be?” This is called the constraint system. Parents pass constraints down; children pass sizes up. A Column might say to its children: "You can be 0–360px wide and 0–640px tall." The children answer back with their chosen sizes. This two-pass negotiation runs through the whole tree.
Paint — RenderObjects record their drawing instructions. But here’s the clever part: they don’t actually draw anything yet. They write instructions into something called a display list — a list of commands like “draw a blue rectangle here, draw this text there.” It’s like writing a to-do list for the GPU rather than doing the work yourself immediately.
Composite — These display lists get assembled into a Scene. Some parts of the UI live on separate layers (think of transparent sheets stacked on top of each other). Scrollable content, animations, and opacity effects often get their own layer so they can be moved or changed without repainting everything else.
Rasterize — The Scene gets handed to Skia (or the newer Impeller engine) which converts those drawing instructions into actual pixels — rows and columns of colored dots — and pushes them to the GPU’s framebuffer. The screen reads from that framebuffer and lights up the correct pixels.
Jargon corner: Rasterization means converting vector drawing commands (like "draw a circle at x=100, y=200, radius=50") into a grid of colored pixels. Framebuffer is a chunk of memory the GPU and display share — the GPU writes pixels there, the screen reads from it.
Layer 3: The Embedder — Plugging into Each Platform
Here’s where the magic of “write once, run anywhere” actually happens — but probably not the way you think.

The embedder is a thin piece of native code that does exactly two things:
- Gives Flutter a drawing surface — a blank canvas (backed by the platform’s graphics API: Metal on iOS/macOS, Vulkan or OpenGL on Android, Direct3D on Windows) where Flutter can put pixels.
- Forwards platform events — touch, keyboard, mouse, resize, and lifecycle events from the OS to the engine.
That’s it. The embedder doesn’t draw buttons. It doesn’t render text. It doesn’t know what your app looks like. It’s just a doorway between Flutter and the hardware.
This is architecturally important: Flutter’s engine doesn’t need to know it’s on a phone. It just needs a surface and events. That’s why Flutter runs identically on six different platforms — the engine is the same; only the doorway changes.The embedder is a thin piece of native code that does exactly two things:
- Gives Flutter a drawing surface — a blank canvas (backed by the platform’s graphics API: Metal on iOS/macOS, Vulkan or OpenGL on Android, Direct3D on Windows) where Flutter can put pixels.
- Forwards platform events — touch, keyboard, mouse, resize, and lifecycle events from the OS to the engine.
That’s it. The embedder doesn’t draw buttons. It doesn’t render text. It doesn’t know what your app looks like. It’s just a doorway between Flutter and the hardware.
This is architecturally important: Flutter’s engine doesn’t need to know it’s on a phone. It just needs a surface and events. That’s why Flutter runs identically on six different platforms — the engine is the same; only the doorway changes.
Layer 4: The Hardware — Where Electrons Become Images
At the very bottom, the GPU receives Flutter’s compiled drawing commands and does the actual math: which pixels light up, with what color, at what intensity. The screen’s refresh circuitry then reads those pixels ~60–120 times per second and illuminates the physical display.
Flutter never tells the screen what to show directly. It tells the GPU how to draw it, and the GPU does the rest. This matters because GPUs are extraordinarily fast at this kind of parallelized pixel math — far faster than any CPU trying to do the same work.
The Big Question: Why Does Flutter Call Itself a “Native Rendering Engine”?
This is the most misunderstood thing about Flutter, and it deserves its own section.

Look at the comparison above. The key insight:
React Native (and similar frameworks) work by translating your JavaScript UI description into calls to the operating system’s built-in UI components — the actual iOS UIButton or Android TextView. This means you're at the OS's mercy. The OS controls how those components look, animate, and behave.
WebView frameworks (like Cordova or Ionic) package a web browser inside your app and render HTML/CSS. You’re not running native anything — you’re running a website in a box.
Flutter is different in a profound way. It doesn’t talk to any native UI component at all. It takes the drawing surface from the OS (just a blank canvas), and then its own engine draws every single pixel — buttons, text, shadows, animations, everything — directly onto that canvas using Skia or Impeller.
This is exactly how a video game works. When you play a game, the game engine draws its own graphics onto the screen — it doesn’t ask the OS to “show a health bar.” Flutter uses the same principle for UI.
Why “native” then?
Because the rendering performance is native. Flutter talks directly to the GPU using the same graphics APIs (Metal, Vulkan, Direct3D) that native apps use. There is no JavaScript bridge slowing things down. There is no HTML layout engine adding overhead. The path from "run this Dart code" to "GPU draws pixels" is extraordinarily short.
The experience feels native because the rendering is just as fast as a native app. The pixels arrive on screen just as quickly. Animations run at the same frame rate. There’s no scrolling jank from a bridge bottleneck.
Why Does Flutter’s Architecture Work Like This?
Let’s address the “why” directly, because this is where most explanations stop.
Why does Flutter have three trees instead of one? Because state management (element tree), UI description (widget tree), and layout math (render tree) have completely different update frequencies. Your widget might rebuild 60 times per second. Your layout might only need recalculation when you rotate the screen. By separating them, Flutter only does the minimum work each frame.
Why is the engine written in C++ instead of Dart? Dart is a wonderful language, but C++ runs closer to the metal (pun intended). Rendering is performance-critical. The difference between C++ and Dart at this level can mean the difference between dropping frames and staying silky smooth. The Dart framework layer is fast enough for application logic; the C++ engine is fast enough for frame-by-frame pixel work.
Why does Flutter draw its own pixels instead of using OS components? Two reasons. First, consistency — if you draw everything yourself, your app looks identical on every device, OS version, and manufacturer skin. No “Samsung changed the button style in their Android fork” surprises. Second, control — when you own the rendering, you can do things like custom shaders, complex blending effects, and non-standard animations that are simply impossible through OS component APIs.
Why does the embedder architecture exist? Because the same engine can be dropped into any platform that can hand it a drawing surface. This is what makes Flutter genuinely cross-platform — not at the “works 80% of the same” level, but at the “literally the same code path for every pixel” level.
Bringing It Together: One Frame, End to End
Here’s what happens in the 16 milliseconds between “user taps the screen” and “new frame appears”:
- The embedder detects a touch event from the OS and sends it to the engine.
- The engine delivers it to the Dart side as a
GestureEvent. - Your framework code calls
setState(). The widget tree rebuilds. - The element tree compares old and new widgets (called diffing) and updates only what changed.
- Layout runs — affected RenderObjects recalculate their sizes and positions.
- Paint runs — changed RenderObjects record new drawing commands.
- The compositor assembles the layer tree into a Scene.
- The Scene crosses to the raster thread, where Skia/Impeller converts it to GPU commands.
- The GPU executes those commands and writes pixels into the framebuffer.
- The display reads the framebuffer and your screen updates.
All of that, 60 times per second. Silently. While your user scrolls through a list.
The Beautiful Reason Flutter Feels the Way It Does
Flutter’s architecture isn’t arbitrary. Every design decision — three trees, C++ engine, own pixel rendering, thin embedder — solves a real problem that other frameworks struggled with.
When you scroll a Flutter list and it’s glass-smooth, that’s the GPU being handed well-formed, pre-optimized draw commands with zero bridge overhead. When your custom animation does something that would be impossible in React Native, that’s Flutter’s complete control over the rendering pipeline. When your app looks exactly the same on a Pixel and a Galaxy, that’s Skia drawing every pixel instead of letting Samsung’s Android skin make decisions.
Flutter isn’t fast despite drawing its own pixels. It’s fast because it draws its own pixels — cutting out every middleman between your code and the GPU.
Want to go deeper? Try exploring Flutter’s source code at github.com/flutter/engine — particularly the
shell/directory, which is where the framework talks to the engine. It's some of the most elegantly organized C++ you'll find in any open-source project.
메타데이터
- post_id
- 772444a420e2
- slug
- how-flutter-actually-paints-your-screen-a-deep-dive-for-every-developer-772444a420e2
- url
- https://medium.com/@themuhmand/how-flutter-actually-paints-your-screen-a-deep-dive-for-every-developer-772444a420e2
- canonical_url
- https://medium.com/@themuhmand/how-flutter-actually-paints-your-screen-a-deep-dive-for-every-developer-772444a420e2
- author_url
- https://medium.com/@themuhmand
- status
- ok
- fetched_at
- 2026-06-22 00:13:37