Chuyển đến nội dung chính

Flutter cold start: measuring the time before your first frame

“The app takes three seconds to open” is a complaint, not a measurement. Three seconds from what — the tap, the process spawning, the engine coming up, or the moment your main() runs? Each of those is a different problem with a different fix, and optimising the wrong one is how teams spend a sprint moving startup from 2.9 s to 2.8 s.

Cold start in a Flutter app has four phases, and they are separable.

The four phases

1. Process launch. The OS creates the process, loads the executable and its shared libraries, and hands control to the platform runner. Nothing in your Dart code affects this. What does affect it is binary size (see the app size discussion) and, on Android, the number of libraries to link.

2. Engine initialisation. The Flutter engine starts, the Dart VM comes up, and — in release builds — the AOT snapshot is mapped in. Plugin registration happens on the platform side here.

3. Dart main() to runApp(). Your code. This is the phase you control completely, and the one people quietly fill with awaits.

4. First frame. Build, layout, paint, rasterise. The user sees something.

The number that matters to a user is the end of phase 4. The number you can move most easily is phase 3.

Measuring it properly

flutter run --profile --trace-startup

This writes start_up_info.json in the build directory with four values in microseconds:

KeyMeaning
engineEnterTimestampMicrosAbsolute timestamp when the engine started
timeToFrameworkInitMicrosEngine start → framework initialised
timeToFirstFrameRasterizedMicrosEngine start → first frame on screen
timeToFirstFrameMicrosEngine start → first frame built

The gap between timeToFirstFrameMicros and timeToFirstFrameRasterizedMicros is diagnostic: if it is large, your first frame is expensive to rasterise (shaders, large images, complex clipping), not expensive to build. Those are different fixes.

Two rules for the measurement to mean anything: profile mode, real device. Debug builds run the Dart JIT and are several times slower to start; a simulator or emulator has different I/O and GPU characteristics from the phone your users have. And measure a genuine cold start — force-stop the app first, not just background it.

For finer detail inside phase 3, add your own timeline events:

Future<void> main() async {
  Timeline.startSync('bootstrap');
  WidgetsFlutterBinding.ensureInitialized();

  Timeline.startSync('prefs');
  final prefs = await SharedPreferences.getInstance();
  Timeline.finishSync();

  Timeline.startSync('db-open');
  final db = await openDatabase();
  Timeline.finishSync();

  Timeline.finishSync();
  runApp(MyApp(prefs: prefs, db: db));
}

These show up as named slices in the DevTools timeline, and they usually end the argument immediately — one of those awaits is almost always 80% of phase 3.

What belongs in main(), and what does not

The default failure mode is a main() that awaits six things before runApp. Every one of those awaits is time on a blank screen.

The test for each initialisation is simple: does the first frame depend on it?

Must be before runAppCan be after
Anything MyApp’s constructor requiresAnalytics and crash reporter (register the handler early, initialise lazily)
The stored theme mode and locale, if you refuse to flash the wrong oneRemote config fetch
A synchronous check of “is the user signed in”Database migrations for screens not shown yet
WidgetsFlutterBinding.ensureInitialized()Push notification registration
Preloading images for screen three

Everything in the right column can move behind the first frame:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());

  // After the first frame is on screen.
  WidgetsBinding.instance.addPostFrameCallback((_) {
    unawaited(_initAnalytics());
    unawaited(_warmCaches());
  });
}

addPostFrameCallback runs after the frame is built. For work that should not compete with the first few frames of animation at all, SchedulerBinding.instance.scheduleTask with a low priority defers it until the scheduler is idle.

For the things that genuinely must be awaited, run them concurrently rather than in sequence:

final (prefs, db, keys) = await (
  SharedPreferences.getInstance(),
  openDatabase(),
  loadSigningKeys(),
).wait;

Three 60 ms awaits in sequence are 180 ms; in parallel they are 60. This one line is frequently the largest single win available, because sequential awaits are the default way people write main().

Heavy synchronous work — parsing a large bundled JSON, deriving a key — belongs in an isolate, not on the startup path of the main thread.

The blank screen, and what to put on it

Phases 1 and 2 happen before any Dart runs, so no Flutter widget can cover them. What covers them is the native splash screen: a launch theme on Android, a launch storyboard on iOS. That is not a hack, it is the platform mechanism, and it is what turns “blank white screen” into “the app is opening”.

The important detail is continuity. If the native splash shows a centred logo on brand blue, the first Flutter frame should be a centred logo on brand blue — then transition. A native splash that hard-cuts to a different Flutter splash looks slower than one screen held slightly longer, because the user perceives the flash as a restart.

A related trap: an app that shows its own animated Flutter splash for a fixed 1.5 seconds has added 1.5 seconds to startup. If the animation is the brand’s, fine — but do not gate it on a timer when the data is already loaded.

The first frame’s own cost

If timeToFirstFrameRasterizedMicros is much larger than timeToFirstFrameMicros, the problem is on the raster thread.

Shader compilation. The first time a particular shader is needed, it must be compiled, and that can stall the frame. Impeller was built specifically to address this class of jank by avoiding runtime shader compilation for its supported cases; on the older Skia backend this was the classic “first run of an animation is janky” cause. Which backend you get depends on platform and Flutter version — check what your build actually uses rather than assuming.

A too-heavy first screen. A home screen that builds forty widgets, decodes six images and lays out a complex grid will take a while to produce. A common, honest fix is to make the first frame cheap on purpose: render the shell — app bar, background, skeletons — immediately, and fill in content on the next frames. The perceived improvement is larger than the measured one, which is fine, because perception is the actual goal.

Large image decodes. Decoding a 4000-pixel hero image blocks. Ship it pre-sized, and use cacheWidth/cacheHeight so the decoder produces only what you display.

A workflow

  1. flutter run --profile --trace-startup on a real device, three cold runs, take the median.
  2. Read start_up_info.json. Decide whether your problem is phase 3 (timeToFrameworkInitMicrostimeToFirstFrameMicros) or phase 4 (build → rasterised).
  3. If phase 3: instrument main() with Timeline, then parallelise or defer.
  4. If phase 4: simplify the first screen, check image decode sizes, check the raster thread in the DevTools timeline.
  5. Re-measure the same way. Record the number somewhere the team can see it, or it will regress within two sprints.

FAQ

Why is my debug build so much slower to start?

Debug builds use the Dart JIT and include the service isolate, observatory and assertions. The ratio to release is large and not proportional — never tune startup against a debug measurement.

Does WidgetsFlutterBinding.ensureInitialized() cost much?

It is fast, and it is required before you touch platform channels (which is what SharedPreferences, path providers and most plugins do). Call it first in main().

Should I preload data during the splash?

Only what the first screen needs. Preloading screen two during the splash trades a measurable startup cost for a benefit the user may never reach.

Is a smaller app faster to start?

It affects phase 1, and it matters most on low-end devices and cold filesystem caches. It is a real effect but usually a smaller lever than a sequential await chain in main().

How do I track this over time?

--trace-startup produces a machine-readable file; run it in CI on a fixed device profile and assert on a threshold. A number in a dashboard is the only thing that stops startup time from drifting back up.


The --trace-startup output fields, binding APIs and splash screen mechanisms described here are documented in the Flutter performance and platform integration guides linked above. The four-phase framing, the “does the first frame depend on it” test, and the advice to make the first frame deliberately cheap are my own judgement from profiling apps this way. Engine behaviour — including which renderer runs on which platform — changes between Flutter releases; measure your own build in profile mode on a real device.


Originally published on FlutterCook. Read the latest version there — that copy is the one kept up to date.

Nhận xét

Bài đăng phổ biến từ blog này

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

2026 👏 Google miễn phí một năm gói AI Plus cho sinh viên Việt Nam 🇻🇳🇻🇳🇻🇳

Chúc mừng anh em 👏 Google miễn phí một năm gói AI Plus cho sinh viên Việt Nam 🇻🇳🇻🇳🇻🇳 Đối tượng là sinh viên đại học từ 18–24 tuổi (bao gồm sinh viên mới và sinh viên từng dùng gói AI Pro 2025). Lưu ý là cần xác minh tư cách sinh viên hàng năm. Khi nhận gói, anh em có được các ưu đãi sau: 🔖 Nâng cấp bộ nhớ đám mây lên 400GB. 🔖 Nhân đôi hạn mức truy cập mô hình AI. 🔖 Mở quyền trải nghiệm tính năng tạo video bằng Gemini Omni. Lưu ý chương trình chỉ dành cho sinh viên đủ điều kiện. Hạn nhận ưu đãi: 31 tháng 12, 2026. Các bạn cần có phương thức thanh toán hợp lệ khi đăng ký. Google AI Plus sẽ tự động tính phí 132.000 ₫/tháng sau khi thời gian dùng thử kết thúc, trừ phi bạn đã huỷ trước đó. Huỷ bất cứ lúc nào. CÁC BƯỚC THỰC HIỆN CHI TIẾT 🔹Bước 1: Truy cập cổng đăng ký chương trình 🔖 Mở trình duyệt và truy cập vào trang chính thức: gemini.google/students/ 🔖 Đăng nhập vào Tài khoản Google cá nhân của bạn. 🔖 Nhấn vào nút "Claim your student plan at no cost" (hoặc Nhận gó...