Mobile Engineering

Flutter Impeller performance regressions on Android: how to prove it’s not shader compilation (and what to measure instead)

Impeller was built to eliminate shader-compilation jank, yet teams still see dropped frames on Android. This piece lays out a measurement-first workflow to identify where the stall actually sits: UI, raster, GPU, or platform.

Impeller was created to remove shader-compilation jank from Flutter rendering, but teams are still reporting frame drops on Android builds that have Impeller enabled.

If your first response is “it must be shader compilation”, you will likely waste a week chasing the wrong thing: on modern Flutter, the stall is often elsewhere in the frame pipeline.

What Impeller actually promised (and what it didn’t)

Impeller exists because shader compilation on the old renderer (Skia pipelines on mobile GPUs) caused user-visible stutters, especially on first run. That motivation is explicit in the engine team’s own FAQ. (flutter.googlesource.com)

But “shader compilation isn’t your main jank source any more” is not the same as “jank is gone”. On Android, Impeller is now enabled by default on API 29+ (Vulkan path where supported), with fallback behaviour on devices that can’t run it. (docs.flutter.dev)

So the decision you actually face is more pragmatic:

  • If your jank was dominated by shader compilation, Impeller often helps.
  • If your jank is dominated by offscreen passes, overdraw, excessive saveLayer usage, image filters, or CPU work in the raster thread, Impeller may not help—and in some cases can be slower in specific backends or device classes.

That second category is where “Impeller regressions” complaints usually live.

The only question that matters: which part of the frame got slow

A Flutter frame is a pipeline. When someone says “frame drops”, you need to force the conversation into measurable terms:

  • Did the UI thread miss its budget (build/layout/paint in Dart)?
  • Did the raster thread miss its budget (recording and dispatching draw ops, layer composition, texture uploads)?
  • Did the GPU miss its budget (the command buffer is fine, but the GPU is late)?
  • Did you block on platform / plugins / I/O on the UI thread?

Flutter’s own guidance is still the right starting point: profile on a physical device, and use profile mode rather than debug mode because debug has JIT pauses and behaviour that is not representative. (docs.flutter.dev)

If you do nothing else, get a reproducible trace that tells you whether you are dealing with UI-bound or raster/GPU-bound work.

Proving it’s not shader compilation: look for the signature

Shader compilation jank has a particular feel: stutters often cluster around “first time we draw X” (first time a gradient + blur + clip combination appears, first time a complex path is drawn, first time a particular shader variant is needed). Historically, on Skia you could mitigate this by pre-warming and bundling shader caches.

With Impeller, that mental model breaks down:

  • The old --cache-sksl / bundle workflow is Skia-specific, and Impeller jank is usually not fixed by trying to apply Skia-era shader cache advice. (This exact confusion shows up frequently enough that people keep writing about it.) (fluttercook.github.io)

So how do you prove it is not compilation?

  1. Reproduce the jank with the scene already “warmed up” (same interaction repeated, after the app has been open for a while).
  2. Compare the same build with Impeller toggled off (Skia) to see whether the jank’s shape changes.
  3. In DevTools / tracing, check whether the long frame is UI time, raster time, or GPU.

If you see repeated spikes on the raster thread or GPU even after warm-up, that’s not shader compilation. It’s steady-state cost.

A workflow that gets you to root cause quickly

This is the workflow we use when we’re trying to answer, quickly and honestly, whether:

  • this is “your code” (fixable in app)
  • this is “engine/backend/device behaviour” (work around, escalate, or disable Impeller on Android)

1) Establish a renderer-controlled A/B

You need a clean A/B where the only thing you changed is the renderer:

  • Run with Impeller enabled (default on many Android configurations now). (docs.flutter.dev)
  • Run with Impeller disabled via flutter run --no-enable-impeller for debugging, or the Android manifest flag for shipped builds. (docs.flutter.dev)

The goal is not “Skia good, Impeller bad”. The goal is to identify which part of the pipeline changes:

  • If UI thread time is identical but raster time changes, you’re looking at renderer/backend behaviour.
  • If UI thread time changes, you’re probably chasing Dart-side work (rebuilds, object churn, synchronous platform calls).

2) Reduce the scene until the failure signature is obvious

Founders hate this step because it feels like engineering theatre. It is not. A 30-line repro is the difference between “we think” and “we know”. Flutter itself asks for small reproducible cases when reporting Impeller issues. (docs.flutter.dev)

Cut down to:

  • one route
  • one scrolling surface
  • one custom paint / shader / image filter effect (if applicable)
  • one interaction that reliably triggers jank (a fling, a pinch zoom, a hero transition)

Once it’s small, you can make a meaningful decision: fix, re-architect, or switch renderer on Android.

3) Capture frame timing, not vibes

Don’t rely on “it looks stuttery on my phone”. Capture per-frame build/raster timing and identify percentiles and outliers.

If your outliers are huge but median is fine, you’re dealing with sporadic stalls (often allocations, uploads, sync points). If your median is already close to the budget, you’re simply over budget all the time (often offscreen passes, expensive filters, too many saveLayers).

When you escalate to engine issues, the Flutter team will ask for this anyway.

Where Impeller Android jank often really comes from

Once you stop blaming compilation, the common culprits tend to look like this.

Offscreen render passes and layer effects

One of the more consistent real-world causes of “Impeller jank” reports is not a shader at all, but an offscreen render pass (rendering a subtree into a texture, then compositing it). This pattern shows up as raster/GPU spikes and can be triggered by combinations of clip/filter/backdrop effects.

If you’re seeing jank around blurs, BackdropFilter, ImageFiltered, or heavy compositing, assume offscreen work until proven otherwise.

FragmentProgram / FragmentShader usage patterns

Flutter’s fragment shader support is explicitly documented as flowing through FragmentProgram assets compiled by the Flutter tool, and then generating one or more FragmentShader instances for rendering. (docs.flutter.dev)

On Android + Impeller, developers are reporting severe frame drops in patterns like:

  • CustomPainter that creates shaders inside paint() (high frequency)
  • shader invalidation inside scrolling lists/grids (high churn)

A representative symptom: large frame drops during shader invalidation inside CustomPainter within a SliverGrid, with mitigations like pre-allocating shader instances rather than creating them inside paint(). (stackoverflow.com)

This isn’t “shader compilation” in the classic sense. It’s object churn and pipeline pressure: you’re repeatedly creating/initialising objects on a hot path, often coupled with heavy overdraw.

Rules of thumb that hold up in production:

  • Do not allocate expensive paint/shader objects in paint() if you can avoid it.
  • Do not combine per-item custom shaders with fast scrolling unless you’ve profiled it on mid-tier Android hardware.
  • If an effect must exist, constrain its bounds (clip) and reduce the area you shade.

Backend/device-specific renderer costs

Some Android regressions are simply backend behaviour. There are active engine issues reporting Impeller slower than Skia on certain Android devices or backends, with reproducible steps and release labels. (github.com)

If your A/B shows Impeller consistently slower on the same device for the same scene, treat it as a product decision:

  • If the regression affects a meaningful slice of your Android install base, disabling Impeller on Android is a legitimate option while you track upstream fixes.
  • If you rely on Impeller-only features (notably custom shader support via ImageFilter.shader), you may need to redesign the effect for Android performance budgets rather than switching renderers. Flutter explicitly notes that the ImageFilter API for custom shaders is only supported by the Impeller backend. (docs.flutter.dev)

What to measure instead (a checklist you can hand to an engineer)

When someone says “Impeller regression”, ask them for these artefacts.

  1. A device list (at least one mid-tier Android, one flagship) and the Android API levels.
  2. Two runs of the same build: - Impeller on - Impeller off (--no-enable-impeller or manifest toggle) (docs.flutter.dev)
  3. For each run: - UI thread frame times (build/layout/paint in Dart) - Raster thread frame times - A trace around the worst frame
  4. A minimal reproduction project (even ugly) that triggers the stall in <30 seconds.
  5. Notes on whether the jank is: - cold-start only - first-interaction only - repeatable in steady state

If you can’t produce (2) and (3), you don’t yet know whether it is a renderer regression or an app-level performance bug.

When the right answer is “disable Impeller on Android”

Disabling Impeller on Android is not heresy. It is sometimes the correct business decision when:

  • the performance regression is repeatable in profile/release on real devices
  • the failure sits in raster/GPU rather than UI
  • you’ve reduced the scene and it’s still slow
  • you are not relying on Impeller-only features

Flutter’s own docs include explicit instructions for disabling Impeller via an Android manifest flag, which is a strong signal that the team expects some developers will need to do this while the backend matures across the device ecosystem. (docs.flutter.dev)

Conversely, if your app depends on Impeller-only rendering features, you may be committing to Impeller and should invest in shaping your UI to its performance characteristics rather than toggling it off.

A brief note on how we help (when it’s actually useful)

At Codeversols, we’ve found the highest-leverage work in cases like this is not “optimise everything”, but to build a minimal repro, get a clean A/B between renderers, and then either:

  • refactor one or two high-cost UI patterns (custom paint, filters, scrolling effects), or
  • package an engine-quality bug report with traces and a project the Flutter team can actually run.

That’s typically faster and cheaper than a broad performance rewrite that doesn’t isolate the stall.

Close

Impeller removed a specific class of shader-compilation jank, but it did not repeal the laws of GPU work, offscreen passes, or hot-path allocations. If you treat every Android stutter as “shader compilation”, you will misdiagnose the problem.

Run a renderer A/B, capture UI vs raster timing on real devices, reduce to a minimal repro, and then measure what is actually slow. Once you can name the bottleneck, the decision—optimise the scene, redesign the effect, file an engine issue, or disable Impeller on Android—stops being guesswork.

More From Our Blog

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy
Mobile Engineering • Oct 16, 2026

Android 16 (API 36) edge-to-edge is enforced: the window insets contract your UI must satisfy

Targeting Android 16 (API 36) removes the practical opt-out from enforced edge-to-edge. The only stable fix is to define a single owner for each inset (status bars, navigation bars, display cutout, IME) and test for double-application in CI.

Read more →
TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)
Mobile Engineering • Oct 15, 2026

TestFlight ‘Waiting for Review’ for days? Ship anyway with a dual‑lane release pipeline (and what to automate in App Store Connect)

When TestFlight Beta App Review and App Store Review queues stall, you don’t need to miss your launch date. This playbook describes a dual‑lane pipeline (internal TestFlight vs external TestFlight vs App Store), deterministic rollout rules, and the App Store Connect automation to keep shipping under review latency.

Read more →
Swift 6 strict concurrency default isolation footguns: how teams accidentally @MainActor their whole app (and a CI check to prove it)
iOS / Swift • Oct 14, 2026

Swift 6 strict concurrency default isolation footguns: how teams accidentally @MainActor their whole app (and a CI check to prove it)

Swift 6’s strict concurrency checks get teams over the line, but one build setting can quietly turn large parts of your app into MainActor-only code. Here’s how that happens, how it shows up as UI jank, and a simple CI audit that catches “main-actor creep” early.

Read more →