DEV Community

Cover image for Why Your App Janks Under Load: Reading CPU Scheduling in Perfetto
Raul Smith
Raul Smith

Posted on

Why Your App Janks Under Load: Reading CPU Scheduling in Perfetto

I first noticed the jank when everything looked healthy.

CPU usage wasn’t maxed out. Memory graphs were flat. Network was quiet. Yet under real usage—scrolling while syncing, navigating while media played—the app started missing frames. Not always. Not predictably. Just enough to make users hesitate before trusting it.

The worst part was that synthetic benchmarks passed. Stress tests passed. Only real load broke the illusion.

That’s when I stopped asking “Why is my code slow?” and started asking “Why isn’t my code running?”

Jank is a scheduling problem before it’s a rendering problem

Most performance conversations start at rendering.

Dropped frames. Choreographer warnings. Missed VSYNC deadlines.

But those are symptoms, not causes.

Before a frame can be rendered, code has to execute. Before code can execute, a thread has to be scheduled. And that scheduling decision happens outside your app.

Jank under load is often the result of CPU scheduling pressure, not inefficient drawing code.

Once I accepted that, Perfetto became far more useful than any UI profiler.

Load doesn’t mean 100% CPU

“Under load” rarely means the CPU is pegged.

It usually means:

  • More runnable threads than available cores
  • Reduced CPU frequency due to thermal state
  • System services competing for high-priority slices
  • Background work overlapping with UI work

From the scheduler’s point of view, this is normal. From your app’s point of view, time quietly disappears.

Perfetto makes that disappearance visible.

What Perfetto shows that other tools don’t

Traditional profilers show where time went.

Perfetto shows when time was denied.

The first time I opened a scheduler track, I saw long gaps between execution slices on threads that were clearly runnable. No blocking calls. No waits. Just silence.

That silence was the cause of jank.

A frame wasn’t late because it was expensive. It was late because the thread didn’t get CPU time when it needed it.

Reading scheduler tracks without lying to yourself

It’s easy to misread Perfetto if you come in with the wrong assumptions.

Key things I learned to look for:

- Runnable but not running
Threads marked runnable but not scheduled indicate contention, not inefficiency.

- Short execution slices
Frequent preemption fragments work, increasing overhead and cache misses.

- UI thread gaps before frame deadlines
If the UI thread doesn’t run early in the frame window, jank is inevitable.

- Background workers overlapping UI windows
Even “low priority” work can steal crucial milliseconds.

Once you align these with frame timelines, the cause-and-effect becomes obvious.

The uncomfortable truth about concurrency

I used to believe concurrency protected the UI.

Offload work. Parallelize. Keep the main thread free.

Under load, that strategy backfires.

Each extra worker thread increases competition. The scheduler now has more runnable entities fighting for the same limited time slices. Context switching increases. Cache locality worsens.

In Perfetto, this showed up as more threads doing less useful work.

Reducing concurrency—fewer workers, tighter scopes—improved frame stability more than any micro-optimization ever did.

Thermal throttling changes everything mid-flight

One of the most misleading things about load testing is temperature.

As the device warms, CPU frequency drops. Schedulers become stricter. Time slices shrink.

Perfetto made this visible through frequency tracks. The same workload that fit comfortably into a frame early in a session no longer did after sustained use.

Nothing in the code changed. The environment did.

This is why jank often appears “after a while” and vanishes on restart.

Why jank appears before crashes

Crashes require failure. Jank only requires delay.

Under scheduling pressure, the system degrades performance long before it kills processes. That’s intentional. It protects user experience at the cost of your assumptions.

This is why jank is an early warning signal. It tells you your execution model no longer fits the system’s priorities.

Ignoring it usually leads to worse failures later.

Designing for scheduling reality

Once I started reading Perfetto traces honestly, my design priorities shifted.

  • UI work moved earlier in the frame window
  • Background tasks became interruptible and resumable
  • Long tasks were split into smaller slices
  • Strict timing assumptions were removed
  • Metrics focused on worst-case frame latency

The app didn’t get “faster” in benchmarks. It got smoother under pressure.

That distinction matters.

Why this matters in production teams

I’ve seen the same scheduling-driven jank surface repeatedly across teams involved in mobile app development San Diego projects, especially where apps coexist with navigation, streaming, fitness tracking, and constant background services.

Modern devices are powerful, but they’re also aggressively managed. The scheduler always wins.

Teams that treat jank as a rendering bug chase symptoms. Teams that treat it as a scheduling signal fix root causes.

The lesson Perfetto teaches quietly

Your app doesn’t run on a CPU.
It runs between other things the system cares about more.

Perfetto doesn’t judge your code. It shows you where your assumptions collide with reality.

When you learn to read CPU scheduling instead of just frame drops, jank stops being mysterious. It becomes predictable.

And once it’s predictable, it’s something you can finally design around.

Top comments (0)