How I Used Perfetto to Separate Real Startup Improvements From Plausible Ones

How I Used Perfetto to Separate Real Startup Improvements From Plausible Ones

By the time I opened Perfetto, the app was already faster.

That was the problem.

The easy version of startup work is when the app is obviously doing too much, the benchmark is ugly, and almost any sensible cleanup buys you something. The harder version comes later, when the app is already in decent shape and the next round of changes can be justified a little too easily.

That is where I was.

I already had startup benchmarks. I already had a baseline profile. A broader cleanup pass had already taken a meaningful chunk out of launch time. The chart looked good enough that I could have stopped there and told a clean story about a modern Android app getting faster.

But I did not trust the remaining startup path enough to stop.

I could measure it, but I could not always explain it. Some work seemed to happen “around startup” without a crisp answer to whether it was actually blocking first draw, safely deferred, duplicated, or just arriving at an awkward time. That kind of uncertainty is where weak performance changes come from. You start editing code not because the evidence is strong, but because the shape of the code makes a theory feel plausible.

That is where Perfetto entered the story.

Not as the hero. Not as the biggest win. As the point where I stopped letting plausible explanations count as evidence.

If you have read the earlier startup piece, this is the follow-up to that story, not a replacement for it. The first article was about finding a meaningful startup win in a modern Compose app and proving it with benchmarks. This one is about the more interesting question that came after that:

which part of the work actually deserved to stay?

This Was A Follow-Up To Real Startup Work, Not The Start Of It

The important context is that Perfetto was not competing against a bad baseline. It arrived after two earlier stages had already done real work:

  • baseline profiles had already shown a repeatable win
  • a broader cleanup pass had already taken a large chunk out of startup

That matters because it changes the standard.

When you are early in startup work, almost any sensible improvement feels exciting. When you are late in startup work, excitement is cheap. You need a way to separate “this looks plausible” from “this survives measurement.”

That is the real reason I value Perfetto here.

If you want the longer backstory on how the app got to that point, read the earlier piece first:

How I Found a 34% Startup Win in a Modern Compose App

It is easy to assume the big startup wins are already gone once an Android app is modern, uses Compose, has reasonable…

levelup.gitconnected.com

This follow-up is about the narrower question the first article only hinted at: once the app is already faster, what does tracing still buy you?

The Benchmark Story Looked Good Before Perfetto Ever Showed Up

Before the tracing pass, the startup work already had a credible shape.

On the original validated tree, the baseline profile alone saved:

  • `112 ms` on the main flow
  • `126 ms` on the setup flow

That is `238 ms` combined across the two startup paths.

Then the broader cleanup pass landed, and that changed the absolute startup numbers much more dramatically. Relative to the earlier validated state, the profiled startup medians dropped by another `248 ms` combined.

That is a substantial result already. It would have been easy to stop there, package the numbers into a clean chart, and call the story done.

I still did not trust the remaining startup path enough to stop.

There were still places where the app felt like it was doing work “somewhere around startup,” without a crisp explanation of whether that work was on the critical path, safely deferred, duplicated, or just showing up at an awkward time.

That is the kind of uncertainty that creates bad follow-up work.

Perfetto Arrived At The Exact Point Where It Could Be Misused

This is the part I do not think performance articles say often enough.

Tracing is easiest to misuse when the app is already in decent shape.

At that point, you open a trace, see a service start early, or a network check, or a callback sequence, and you can easily talk yourself into a “clever” lifecycle rewrite that looks technical and disciplined. Sometimes it is a good change. Sometimes it is just a fancy way to create new uncertainty.

That was the risk here.

I did make some changes during this stage that looked plausible and did not earn the right to stay. That was not a failure of the process. That was the process working.

The useful outcome was not “Perfetto showed me a giant hidden bottleneck.” The useful outcome was:

  • it showed which work was really happening before first draw
  • it verified that some checks could move safely after first draw
  • it showed when a service-side idea was still too noisy or inconclusive to keep

That is a much more mature kind of win than a chart alone can show. It is less satisfying on a slide, but much more useful in a real codebase.

The trace labels themselves were simple:

If the embedded code does not load, open it on GitHub Gist.

This was not sophisticated instrumentation. It was just enough structure to stop talking vaguely about “startup work” and start asking which slice was actually landing before first draw.

The Best Perfetto-Driven Change Was Small, Specific, And Boring

The most defensible tracing-driven optimization was not some grand architectural rewrite.

It was moving a cluster of home screen startup checks out of the visible launch path:

  • the initial file watcher
  • the initial network validation
  • the first server ping

Those checks still mattered. They just did not need to compete with first draw.

Once the traces were in place, the question became much simpler:

are these checks blocking visible startup for a good reason, or are they just happening early because nobody ever forced the issue?

The answer was the second one.

So the change was to let the first frame land, report fully drawn, and then enable that first round of monitoring and connectivity work. Normal traces after that change showed the slices where they should have been: after first draw, not before it.

The pseudocode version of that change is here:

If the embedded code does not load, open it on GitHub Gist.

That is not the sort of optimization that gets people excited on social media. It is exactly the sort of optimization that tends to survive contact with reality.

The Most Valuable Perfetto Contribution Was Rejecting Work

This was the real lesson for me.

The raw savings from the final Perfetto-guided pass were positive, but smaller than the earlier stages:

  • `142 ms` combined on the `verify` side
  • `44.5 ms` combined on the `speed-profile` side

If you stare only at those numbers, you can make Perfetto sound optional. I think that misses the point.

By this stage, the app was already much healthier. The remaining job was not to discover one more giant win. It was to keep the last round of work honest.

Perfetto helped do that by making it possible to say:

  • this change clearly moved work off the critical path, keep it
  • this change improved attribution without changing behavior, keep it
  • this service-lifecycle tweak is not producing a clean enough result, revert it

One example of that last category was a deferred runtime startup path that looked reasonable on paper:

If the embedded code does not load, open it on GitHub Gist.

That kind of change is exactly where tracing was useful. It was easy to invent a tidy explanation for why moving service and client startup later should help. It was much harder to prove that the resulting trace was cleaner, quieter, and worth the added lifecycle complexity. In this case, that standard mattered more than whether the idea sounded disciplined.

That is why I would not describe this stage as “Perfetto made startup fast.” Cleanup had already done more of that. Baseline profiles had already done more of that.

I would describe it like this instead:

Perfetto made the remaining startup work strict enough that only the defensible changes survived.

That is not flashy, but it is exactly what late-stage performance work needs. Late-stage work is rarely about heroics. It is about becoming harder to fool.

The Step-By-Step Comparison Is More Interesting Than The Final Number

The final startup number is good, but the sequence is better.

What the sequence shows is that different tools paid off in different ways:

  • baseline profiles delivered the first clean, measurable win
  • cleanup work delivered the biggest absolute savings
  • Perfetto delivered smaller direct gains, but much better decision quality

That is the part I would want another team to take away.

There is a tendency to assume the last tool in the story deserves the most credit. That is how people end up talking about tracing as if the trace itself were the optimization.

It is not.

The trace is the thing that gives you enough visibility to know whether the optimization deserves to exist.

What This Follow-Up Changed In My Head

The earlier startup article left me with one broad conclusion: a lot of meaningful startup work does not begin as “startup optimization.”

This follow-up added a second conclusion:

once the big wins are gone, the hardest part is often refusing to keep the changes that only looked smart.

That is why I now think of Perfetto less as a “find the bottleneck” tool and more as a truth serum for late-stage performance work.

It is still useful earlier than that, of course. But its value here was not that it revealed some shocking secret. Its value was that it tightened the standard.

It forced the final round of changes to answer harder questions:

  • did this actually move work later?
  • did it help the critical path or just shuffle code around?
  • did the benchmark improve?
  • if the trace is still ambiguous, why am I keeping this?

Those questions saved me from keeping more weak changes than the trace directly saved in milliseconds. That may be the most honest way to describe the value of tracing once the obvious wins are gone.

What I Would Repeat Next Time

If I were doing this again on another app, the sequence I would trust is:

  1. Benchmark the real startup paths, not just one launcher case.
  2. Validate baseline profiles instead of assuming they help.
  3. Do the cleanup work that makes the startup path healthier.
  4. Add tracing when the next question is no longer “is startup slow?” but “why is this still happening here?”
  5. Keep only the changes that improve either startup time or startup clarity enough to justify their complexity.

That last clause matters.

I do not think every performance change needs to pay off in raw milliseconds. Some are worth keeping because they make the system easier to reason about and future regressions easier to catch.

But even then, I still want them to earn their keep.

Perfetto did.

Not because it was the biggest startup win, but because it helped make the final startup story believable.

Closing Thought

I think that is the part performance stories often flatten too aggressively. We like the version where one technique wins, one chart proves it, and the lesson fits neatly into a conclusion.

That is not what this one felt like.

This one felt like getting the obvious gains first, then discovering that the last stretch of performance work was less about finding one more miracle and more about refusing to lie to myself. Refusing to keep changes because they sounded advanced. Refusing to give a trace credit for savings it did not create. Refusing to treat “probably better” as the same thing as “measured and explained.”

That is why I still think Perfetto was worth doing, even though it was not the biggest source of startup savings.

Baseline profiles got the first clean compilation gain. Cleanup did most of the heavy lifting. Perfetto made the final round of work strict enough that I trusted what was left standing.

And in the long run, that may be the more valuable result anyway.

A faster startup is good. A startup path you can actually explain is better. A workflow that teaches you which changes are real is the part you get to keep on the next app.