skip to content

Users of a Flutter meditation app report that the breathing animation stutters only the first time after install; how do you decide whether Impeller should have prevented it?

level: seniorimportance: should knowfreq 35%

answer

  1. first run only, then smooth
  2. classic shader-compile signature
  3. which renderer did the device use
  4. below API 29 still Skia
  5. other first-run costs remain

basics

~20 s

First-run-only stutter is the signature of just-in-time shader compilation, which Impeller removes. So check which renderer the affected devices use (Android below API 29 still runs Skia); on Impeller devices, hunt other one-time costs such as image decoding.

solid answer

~50 s

"Smooth after the first time" matches Skia's just-in-time shader compilation: the first frame using a new effect compiled its shader and missed its deadline. Impeller compiles its shaders when the engine is built, so on **iOS** (Impeller only) and **Android API 29+** this cause should not occur. Step one is therefore to group the reports by device: Android below API 29 renders with Skia on OpenGL ES and can still show the old jank, and logcat on a test device shows whether Impeller (`Vulkan` or `OpenGLES`) was selected. If the affected devices do run Impeller, look for other one-time work: decoding a large background image at full resolution, loading audio or Lottie assets, first-time font loading, or heavy synchronous setup when the screen opens. Reproduce on a freshly installed profile build on a real device, since debug builds and warm devices hide or distort it.

code

dart · 7 lines
dart
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  // Decode the background before the breathing animation starts,
  // so its first frame does not also pay for image decoding.
  precacheImage(const AssetImage('assets/images/dusk.jpg'), context);
}

go deeper

for a junior

Recall that stutter only on first use, then smooth, used to mean shaders compiling, and that Impeller was built to remove it.

for a middle

Explain why the stutter is once per install, and list the other one-time costs such as image decoding and asset loading that look the same.

for a senior

Triage by device and renderer first, confirm Impeller via logcat, reproduce cold on a profile build, and fix with precaching or moving work off the frame rather than renderer switches.

for a principal

Decide how much of the old-device population justifies extra work on the Skia path, and set a first-run performance check for fresh installs in release testing.

## The report A meditation app opens a breathing exercise: a large circle expands and contracts over a blurred gradient background, synchronized with a soft animation curve. Users say the circle "jerks" for a moment the very first time they open the exercise after installing the app; afterwards it is always smooth. The team's own devices never show it. ## Why "first time only" is a diagnostic signature A problem that happens once per install and then disappears points at something that is **computed once and cached**. Before Impeller, the textbook cause in Flutter was **shader compilation**: the Skia renderer compiled the GPU shaders for a combination of effects (a gradient, a blur, a clip) the first time that combination was drawn. Compiling could take longer than a frame, so the first frame stalled; the result was cached on the device and never repeated. Developers' warm devices hid it. Impeller was built to remove exactly this: it compiles a fixed, smaller set of shaders **when the engine is built** and creates its pipelines upfront, so there is no runtime shader compile for built-in rendering. So the question becomes: **was Impeller actually rendering on the devices that stutter?** ## Step 1: which renderer did the affected devices use? | Device | Renderer in 3.47 | Can show shader-compile jank? | |---|---|---| | iPhone or iPad | Impeller on Metal, the only option | not from built-in shaders | | Android API 29+ | Impeller on Vulkan, or Impeller OpenGL ES | not from built-in shaders | | Android below API 29 | legacy Skia on OpenGL ES | **yes** | | any app launched with Impeller disabled | legacy Skia | **yes** | Practical checks: - Group crash-free performance reports or support tickets by OS version. If they cluster on Android 9 and older, the old mechanism is back in play there. - On a test device, look for the logcat line `Using the Impeller rendering backend (Vulkan).` or `(OpenGLES).` Its absence means Skia. - Check that nobody shipped the Android manifest opt-out (`io.flutter.embedding.android.EnableImpeller` set to `false`), which forces Skia on every device. ## Step 2: if Impeller was rendering, find the other one-time cost Several first-run costs look identical to a user: 1. **Image decoding.** A large background photo decoded at full resolution the first time the screen opens. Sizing the decode to the display (for example with `cacheWidth` on `Image.asset`) and precaching the image before the exercise starts both help. 2. **Asset and animation loading.** Reading and parsing an audio file or an animation file on first use. 3. **Fonts.** A custom font loaded the first time text in it is shown. 4. **Heavy synchronous setup.** Building a large data structure or parsing JSON in `initState` or the first `build`, which blocks the frame on the UI thread. These are fixed by moving the work earlier (precache during a splash or the previous screen) or off the frame (another isolate), not by renderer settings. ## Step 3: reproduce it the way users see it - Use a **profile build** on a **real device**; debug builds are slower for unrelated reasons. - **Reinstall or clear app data** before each attempt so caches are cold. - Record a performance trace of that first run; DevTools marks frames that compiled shaders separately from other slow frames. Reading those charts in depth belongs to performance tooling. ## Communicating the finding Whatever the cause, write it down with the device class it affects: "Android 9 and older, Skia path, one-time shader compile" or "all devices, 4K background decoded on first open". That tells the team whether the fix is a code change for everyone or an accepted limitation on an old slice of devices. ## What not to do - Do not reach for Skia-era **SkSL warm-up**: the tool's SkSL bundling was removed in the 3.32 cycle, and Impeller has nothing to warm. - Do not add a custom `ShaderWarmUp` by default. `PaintingBinding.shaderWarmUp` is `null` by default, and warm-up only helps where Skia renders, at the cost of startup time. - Do not disable Impeller as a "fix"; it reintroduces the very jank you are chasing. ## Summary - First-run-only stutter was Skia's shader-compile signature. - Under Impeller that cause is gone, so first confirm the renderer per affected device. - On Impeller devices, hunt other once-per-install costs and move them earlier or off the frame.

  • Reports cluster on Android 9 devices. What can the team realistically do?
    Those devices render with Skia on OpenGL ES, so first-use shader compilation can still stall frames. Options are limited: simplify the costly effects on that screen (fewer blurs and layered opacity), accept a one-time stutter, or experiment with a custom ShaderWarmUp that draws the same effects at startup, measuring the startup cost it adds.
  • Why can the team not reproduce the stutter on their own phones?
    Anything computed once and cached, such as decoded images or, on Skia devices, compiled shaders, is already warm on a device that has opened the screen many times. Reproducing requires a cold state: reinstall or clear app data, use a profile build, and test on the affected device class.

saying these in an interview costs you the question

  • Impeller removes every kind of first-run stutter automatically.
  • Disabling Impeller will fix first-run animation jank.
  • Bundling SkSL shaders is the current fix for first-run stutter.
  • A debug build on an emulator is good enough to reproduce first-run jank.
  • All Android devices in Flutter 3.47 render with Impeller.