Most developers approach Flutter assuming it works like React Native or Xamarin—wrapping native UI components and calling platform APIs. But that assumption is wrong, and it's the key reason Flutter feels so smooth. Let me walk you through how Flutter actually works, from your Dart code all the way to pixels on screen, and why that architecture makes it fast by default.
Understanding the three-layer architecture
How Flutter works comes down to three distinct layers working in concert: the framework, the engine, and the embedder. Think of it like a sandwich. Your Dart code sits at the top, the native platform sits at the bottom, and Flutter's layers bridge everything in between.
When you write a Flutter app, you're not writing for iOS or Android directly. You're writing for the Flutter framework. That framework—built in Dart—manages your widgets, handles layout and rendering calculations, and orchestrates everything that happens during a frame. The engine, which is the performance-critical layer written in C++, executes those rendering commands, manages the Dart runtime, and talks to the graphics API. Finally, the embedder is the thin platform-specific layer (separate for iOS, Android, web, macOS, Windows, Linux) that handles system integration, threading, and native plugin dispatch.
Here's why this matters: Flutter doesn't ask iOS to draw a button for you. Flutter asks the engine to draw a button, and the engine tells the GPU directly. No intermediate translation. No waiting for native frameworks to do work. This is fundamentally different from React Native, which wraps native components and translates your JavaScript into native calls.
Flutter draws every pixel itself
This is the architectural decision that makes Flutter different. Instead of wrapping native UI components—buttons, text fields, navigation stacks—Flutter reimplements the entire visual stack.
When you use a Flutter Button or TextField, you're not getting a native Android button or iOS UIButton. You're getting a Flutter widget that is rendered as pixels on a canvas. Flutter controls the color, the shape, the shadow, the text rendering, the animation, everything. This sounds like it would be slower, but it's actually faster because:
- No bridge crossing between Dart and native code for every UI state change
- No platform framework overhead deciding how to layout and render
- Direct control over the rendering pipeline means predictable, consistent performance
- One rendering path for all platforms (iOS, Android, web, desktop) means fewer bugs and easier optimization
The tradeoff is that Flutter widgets need to look and feel native on each platform. That's why Flutter provides Material widgets (Android-style) and Cupertino widgets (iOS-style). But under the hood, both are rendered the same way: as pixels you control completely.
Consider a real scenario: you're building a ride-hailing app with rapid state updates—user location changing, driver status updating, prices recalculating. With React Native, each state change would trigger bridge calls to native frameworks. With Flutter, the framework recalculates the widget tree, the engine renders the changes, and you're done. All in one thread, predictable timing.
The engine: Impeller and the Dart runtime
The Flutter engine is where the actual rendering happens. It's a C++ codebase that handles two critical responsibilities: running your Dart code and drawing to the screen.
For the graphics side, Flutter historically used Skia, a proven 2D graphics library. But Skia has a problem: shader compilation happens at runtime, which can cause frame drops when a new shader combination first appears on screen. This is called shader compilation jank, and it's been a real pain point in production apps.
That's why Flutter introduced Impeller, a new rendering engine designed from scratch to eliminate that jank. Impeller pre-compiles shaders offline (at build time), so there's no runtime shader compilation. Your animations stay smooth, your scrolling stays smooth, even with complex visual effects.
The engine also manages the Dart runtime. Your Dart code doesn't run on the platform's default runtime. It runs inside Flutter's own Dart VM (in development and debug modes) or ahead-of-time compiled to native code (in release mode). This is significant because it means Flutter controls the garbage collector, the memory layout, the execution model—everything that affects performance.
On the threading side, Flutter uses a multi-threaded architecture. The platform thread handles native integrations and platform events. The UI thread runs your Dart code and produces frames. The render thread talks to the GPU. The I/O thread handles file and network operations. This separation means long-running I/O doesn't block your UI, and your UI thread doesn't block the GPU.
The platform embedder: thin and focused
The embedder is intentionally minimal. For Android, it's a small Kotlin/Java wrapper that creates a FlutterEngine, wires up lifecycle events, and handles method channels for platform-specific features. For iOS, it's similar—a thin Swift wrapper around the C++ engine.
When you need native functionality (accessing the camera, reading location, using biometrics), you don't use native UI. You send a method call through a channel to native code, which does the work and returns the result. Your Flutter UI never knows it happened.
Here's a practical example. I built a travel app that needed to book flights through a platform-specific payment system. Instead of rendering a native payment UI and trying to sync state back, we called a platform method, let native code handle everything, and got a result back:
static const platform = MethodChannel('com.example.travel/payments');
Future<void> initiatePayment(String orderId) async {
try {
final result = await platform.invokeMethod('processPayment', {
'orderId': orderId,
'amount': 150.00,
});
print('Payment result: $result');
} on PlatformException catch (e) {
print('Payment failed: ${e.message}');
}
}
The embedder passes that call to native code, which handles the complexity, then returns. Your Flutter UI stays in control. No native UI ever appears unless you explicitly show it.
Compiled to native code in release builds
Debug builds run your Dart code on the Dart VM, which is slower but lets you hot-reload and use debugging tools. Release builds are different: your Dart code is ahead-of-time compiled to native machine code (ARM64 on Android, ARM64 on iOS, x86 for web).
This is why release builds are so fast compared to debug builds. There's no interpreter overhead. Your Dart code is native code, running directly on the CPU. The whole app—framework, engine, your code—compiles to a single efficient binary.
For web, Flutter compiles Dart to JavaScript using dart2js, then further optimizes it. It's not as fast as native, but the rendering model stays consistent: Flutter still draws to a canvas, still controls every pixel.
The compilation process also enables optimizations like tree-shaking (removing unused code) and constant folding (evaluating expressions at compile time). That's why Flutter release APKs are typically 15-20 MB, not massively larger than native apps.
Common mistakes
- Treating Flutter like React Native. Don't assume you can drop native UI components. Flutter renders its own UI. Accept it, and you'll write better apps.
- Running expensive computations on the UI thread. Flutter is single-threaded for Dart, just like any UI framework. Long-running tasks (JSON parsing, database queries, crypto operations) should run in isolates, not block the UI thread.
- Ignoring Impeller in production. If you're still on Skia and seeing jank during animations or scrolling, switch to Impeller. It's been stable for a while and solves real problems.
- Assuming platform embedders are "free". Don't do heavy work in the platform thread. Minimize native code. Use it for integration, not business logic.
- Not profiling release builds. Debug performance is irrelevant. Profile your release build. That's what users see.
Key takeaways
- Flutter's three-layer architecture—framework, engine, embedder—separates concerns and eliminates bridge overhead that plagues cross-platform frameworks.
- Flutter renders its own UI instead of wrapping native components, giving you control and consistency but requiring platform-specific styling (Material, Cupertino).
- Impeller pre-compiles shaders at build time, eliminating runtime shader jank and delivering smooth animations on every frame.
- Release builds are ahead-of-time compiled to native code, delivering performance that matches native apps without the maintenance burden.
- The platform embedder is intentionally thin; keep native code minimal and use method channels for integration, not UI.






Comments
Loading comments…