To make an Android app start faster, the main idea is to do less during startup. In practice, we usually reach for:
- Asynchronous Loading
- Delayed Loading (DelayLoad)
- Lazy Loading
If you have worked on startup optimization, you have probably used these already. Here I want to look at one DelayLoad implementation and why it behaves as it does. The code is simple, but following it takes us through Looper, Handler, MessageQueue, and VSYNC. I’ll also discuss the problems I ran into and some thoughts along the way.
Version note (reviewed 2026-10-13): This is a historical 2015 experiment; its screenshots and three approaches are retained. Neither “two posts guarantee execution after the first frame is visible” nor “startup always needs two performTraversals calls” is a valid API guarantee. The discussion checks AOSP Android 6.0 and 16 queue behavior and adds layout callbacks, Android 10 / API 29 frame-commit callbacks, and current measurement guidance. The screenshots establish only the observed ordering in those samples, not universal presentation timing.
1. Implementation of Optimized DelayLoad
A common DelayLoad approach is Handler.postDelayed in onCreate. It sets an earliest eligible execution time, not a signal that the interface is ready. Queue contention, a busy main thread, and deep sleep can delay it further. The first question is therefore: how long should it wait?
The same fixed delay can behave differently across devices and startup states; fast hardware alone does not guarantee a particular initialization time.
Here is the original nested-post approach; its limits are discussed below:
- Create a
Handlerand aRunnablefor UI updates.
1 | private final Handler myHandler = new Handler(Looper.getMainLooper()); |
- Add the following code to your main Activity’s
onCreate:
1 | getWindow().getDecorView().post(new Runnable() { |
This explicitly uses the main Looper (import android.os.Looper). Both Runnables still run on the UI thread; posting does not create a background worker. Expensive work can still block input and subsequent frames, even if it starts later. Now compare the three approaches.
2. Comparison of Three Approaches
To compare the three approaches, I wrote a small app with three differently sized images and a TextView under each to show its dimensions. getWidth() / getHeight() report View layout dimensions in pixels, not the intrinsic dimensions of a Drawable or Bitmap. The complete example below explicitly binds the main thread, balances tracing, and removes pending callbacks when destroyed. It has been edited and is not a verbatim copy of the linked repository.
Use an API 21+ project already configured with AndroidX AppCompat and an AppCompat theme. Adjust the package and declare this Activity in the manifest. Select one MODE per run and relaunch; 300ms and the later 50ms are comparison settings, not recommended production delays.
1 | package com.gracker.delaytest; |
Use this matching res/layout/activity_main.xml and place the repository’s sample zhihu.png in res/drawable/. The sizes create three distinct layouts; they do not reproduce the historical screenshots’ pixel values. Enable DUMP_QUEUE only for debugging, and leave it off for performance comparisons.
1 |
|
Keep three questions separate:
- When is
updateTextexecuted? - Have the ImageViews been laid out, and do the values reflect that layout?
- Has work left the initial-display critical path, and does it delay later usability?
2.1 Approach 1: Immediate Post
When is it executed?
In this capture, updateText follows the Activity startup callbacks and precedes the first traversal. That is this sample’s ordering: Handler.post() is not a dedicated “after onResume” or “after layout” API.
Are dimensions correct?
The screenshot shows 0 because these Views had not yet been laid out when read. Completion of onCreate/onStart/onResume does not guarantee measurement or layout; initial dimensions are normally established in a later traversal. A legitimate layout can also have zero dimensions, so 0 alone does not diagnose the same issue in every app.
Is there a DelayLoad effect?
Here the task still ran before the first traversal and could lengthen the initial-display path. There is no universal requirement for two traversals before the first frame. Traversal count depends on window state and other conditions, and finishing CPU drawing is not proof of GPU completion or presentation.
2.2 Approach 2: PostDelayed (300ms)
When is it executed?
In this sample, updateText follows the two early traversal sections, and setText causes another UI update. Those CPU sections alone do not precisely establish when the first frame was displayed.
Are dimensions correct?
Yes in this capture: the ImageViews had been laid out. These are their current layout dimensions, not the source image dimensions.
Is there a DelayLoad effect?
It depends on the delay and that run’s load. I recorded about 170ms of extra waiting at the time. The cropped image lacks a full time axis and presentation track, so that number remains a note from the experiment; it cannot be remeasured from this image.
Here we are only updating a TextView. With a large part of the first screen, the user may see blank space followed by content popping in, perhaps with a bonus flash. Expensive work may also stall later interactions. That is hardly the effect we want.
Why not just shorten the delay? The same 300ms behaves differently across devices, startup states, and scheduling loads. I changed it to 50ms and captured another trace:

Here updateText falls between the early traversal sections. Layout dimensions were available, but that does not establish that the first frame was already visible. Available dimensions, completed UI-thread work, and visible pixels are separate conditions.
2.3 Approach 3: Optimized DelayLoad
After trying the first two approaches, I wanted to move the task later without guessing a delay in milliseconds. This third approach was that attempt. It changed the ordering in this experiment, but nesting two posts does not mean “run after the first frame is visible.”
When is it executed?
In this historical trace, updateText follows the later traversal while RenderThread work is still in progress. The TextView change still needs scheduling and rendering; it is not guaranteed to appear at exactly the next VSYNC.
Are dimensions correct?
The dimensions had been established in this run:

Is there a DelayLoad effect?
It deferred the task in this sample without a fixed millisecond value. It does not guarantee post-presentation loading across Android versions, attachment states, or window lifecycles.
3. Reflections
The code is small, but understanding the observed order requires looking at Handler/MessageQueue/Looper, View attachment, and ViewRootImpl traversal scheduling. The next part follows those details. A successful delay in one capture is not a stable first-frame callback.
4. Code
The historical project is DelayLoadSample. The checked snapshot is 62e03ae (2020-02-25): it uses one image and defaults to an IdleHandler example, unlike the three pairs here. Its height label incorrectly calls getWidth(); use getHeight() instead, as in the completed example above. Its old Gradle, SDK, and dependency configuration is historical, not a promise of compatibility with current tools.
5. 2026 Update: Choose the Condition You Need
For layout dimensions, use a layout callback. This Kotlin helper uses AndroidX Core’s doOnLayout (available since 1.1.0). Call it on the UI thread for each bound ImageView/TextView pair. If layout is already complete with no pending layout request, it runs immediately; otherwise it runs once after the next layout and removes its listener. It does not wait for presentation and should not contain expensive initialization.
1 | import android.widget.ImageView |
For pre-draw or frame submission, distinguish the signals. OnPreDrawListener runs before drawing. Android 10 / API 29 registerFrameCommitCallback reports rendered content submitted to the swap chain and only works with hardware rendering; the frame may still not be visible. Register before the target frame and remove unfired callbacks as required by the lifecycle. It is not an exact presentation-complete notification.
For deferring nonessential work, first identify dependencies. Keep only short UI-required work on the main thread. Handler posts and IdleHandler do not create background work; queue idleness does not mean the first frame is visible. Run CPU/IO work on an appropriate executor and manage cancellation. Primary-content loading still contributes to the user’s wait. AndroidX App Startup manages initialization dependencies and entry points, not post-frame scheduling. For manual lazy initialization, remove its automatic initialization configuration and check that another initializer does not initialize it early as a dependency.
The third approach is not a contract: in AOSP android-6.0.0_r1, an unattached View posts into ViewRootImpl.getRunQueue(), forwarded to a Handler during traversal. In android-16.0.0_r1, each View has its own queue, forwarded during attachment. Attached Views post directly to their associated Handler in both versions. Posting again creates another queued task; it does not wait for the GPU or display.
Measure initial display separately from full usability. Macrobenchmark StartupTimingMetric (since 1.1.0) exposes timeToInitialDisplayMs and timeToFullDisplayMs, the latter based on the fully-drawn signal. It ends when the frame containing or following that signal finishes rendering. This metric may be unavailable before API 29; a missing value is not zero. Report fully drawn when primary content has rendered and meets the product’s usable-state definition, not unconditionally in onCreate. Compare repeated cold/warm/hot starts with device, build, data, and compilation state held constant, and inspect subsequent frames. Moving work past the first frame alone does not establish a successful optimization.
Baseline Profiles guide ART precompilation; Startup Profiles improve DEX layout at build time. Neither schedules tasks. DEX layout optimization is available from AGP 8.1 and enabled by default from AGP 8.3, with the required configuration, R8, and generated profiles. No fixed speedup is promised. Android 12 / API 31 onward also supports correlating app, GPU, and display work through Perfetto FrameTimeline.
Sources
- AOSP 6.0 View.java, ViewRootImpl.java
- AOSP 16 View.java, Handler.java
- ViewTreeObserver, doOnLayout
- App Startup, StartupTimingMetric, App startup time
- Baseline Profiles, Startup Profiles, FrameTimeline
About Me && Blog
Below are my personal details and links. I look forward to connecting and sharing knowledge with fellow developers!
- About Me: Includes my WeChat and WeChat group links.
- Blog Navigation: A guide to the content on this blog.
- Curated Android Performance Articles: A collection of must-read performance optimization articles. Self-nominations/recommendations are welcome!
- Android Performance Knowledge Planet: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.
“If you want to go fast, go alone. If you want to go far, go together.”
