A Flutter screen with a BackdropFilter frosted-glass header stutters while its list scrolls underneath; how do you diagnose and cut that raster cost?
answer
- raster bar high, UI bar low
- the blur reruns every scroll frame
- no clip means the full screen
- BackdropGroup and BackdropFilter.grouped
- ImageFiltered for one static child
basics
~20 sProfile on a real device: a high raster bar with a low UI bar points at the blur. Bound it with a ClipRect, shrink area and sigma, share one backdrop via BackdropGroup, and use ImageFiltered when only one child needs blurring.
solid answer
~40 sFirst confirm it: in profile mode on a low-end device the DevTools frame chart shows raster time over budget while UI time stays low, and toggling the filter's `enabled` to false makes raster time drop. A backdrop blur is expensive because it reads what is already painted behind it and re-blurs it; with a list scrolling underneath, that happens every frame. Then cut it: wrap the `BackdropFilter` in a `ClipRect` sized to the header, because without an ancestor clip the filter applies to the whole screen; lower the blur sigma and the blurred area; for several non-overlapping frosted bars use one `BackdropGroup` with `BackdropFilter.grouped` (Flutter 3.29+); avoid an `Opacity` above it; and if the thing behind is static, blur that child with `ImageFiltered` or a pre-blurred asset.
code
dart · 28 linesimport 'dart:ui' as ui;
import 'package:flutter/material.dart';
class FrostedHeader extends StatelessWidget {
const FrostedHeader({super.key, required this.title, this.blur = true});
final String title;
final bool blur;
@override
Widget build(BuildContext context) {
// ClipRect bounds the blur to the header instead of the whole screen.
return ClipRect(
child: BackdropFilter(
enabled: blur, // switch off on weak devices instead of a zero sigma
filter: ui.ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: Container(
height: 72,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 16),
color: Colors.white.withValues(alpha: 0.6),
child: Text(title),
),
),
);
}
}go deeper
Know that BackdropFilter blurs what is behind it, that it is expensive, and that it needs a ClipRect to limit where it applies.
Explain why a blur over moving content is recomputed every frame and why that shows up on the raster thread rather than in build.
Walk the diagnosis in profile mode, A/B the filter with enabled, then apply clip, sigma, grouping and ImageFiltered in order of payoff.
Own the device-tier policy: which surfaces may use live blur, what the fallback looks like, and how the choice is verified on entry-level hardware.
## Why a frosted header is expensive A **`BackdropFilter`** does something unusual: instead of filtering its own child, it applies an `ImageFilter` (typically `ImageFilter.blur(sigmaX:, sigmaY:)`) to **everything already painted behind it**, then paints its child on top. To do that the engine must read back the pixels underneath, run a blur over them and composite the result, work that sits on the **raster thread** and uses offscreen buffers. A blur is a **non-local** filter: each output pixel depends on a neighbourhood of input pixels, and a larger sigma means a larger neighbourhood. The Flutter API docs call the effect relatively expensive, especially for non-local filters such as blurs. The scrolling list makes it worse. When the content behind the header moves, the blurred result is different on every frame, so nothing can be reused; the blur is recomputed at the full frame rate for as long as the user scrolls. ## Step 1: prove it is the blur 1. Run in **profile mode** on a real, preferably low-end, device; debug-mode timings are not representative. 2. Open the DevTools Performance view and scroll. A **raster** bar over the frame budget with a **UI** bar comfortably under it says the cost is in rasterizing, not in building. 3. A/B the suspect: set the filter's `enabled` parameter to `false` (the docs recommend this over a "no-op" filter) and scroll again. If raster time drops sharply, the backdrop filter is the cost. ## Step 2: cut the cost | Cause | Fix | |---|---| | No clip around the filter | Wrap it in `ClipRect` (or `ClipRRect`) matching the header; without an ancestor clip the filter covers the **full screen** | | Large sigma, large area | Reduce `sigmaX`/`sigmaY` and the header height; test the smallest blur that still reads as frosted | | Several frosted surfaces | Put them under one `BackdropGroup` and build each with `BackdropFilter.grouped`, so the engine reads the backdrop once (Flutter 3.29+) | | The blurred content is a static image | Blur the image itself with `ImageFiltered`, or ship a pre-blurred asset | | An `Opacity` above the filter | Remove it; its offscreen layer adds cost and can change how the blend looks | Shared backdrop keys come with one caveat from the docs: filters that **overlap** each other should not share a key, or the overlap looks as if only one blur were applied. ## Step 3: decide what is acceptable - **Device tiers.** A blur that holds 120 Hz on a flagship may not hold 60 Hz on an entry device. A common compromise is a translucent solid color (`Colors.white.withValues(alpha: 0.85)`) as the fallback, switching the blur off through `enabled`. - **Motion only.** Some apps keep the blur while idle and disable it only during fast scrolling; weigh the visual pop against the saved frames. - **Measure after each change.** Clip first, since it is often the whole problem, then sigma, then grouping. ## Things that do not help - A `RepaintBoundary` around the list does not make the blur cheaper: the pixels behind the header still change every frame. - `const` widgets reduce rebuilds, which were never the cost here. - Swapping the blur for a `ShaderMask` or `ColorFiltered` trades one offscreen pass for another.
- In Flutter, when should you use ImageFiltered instead of BackdropFilter?When you want to blur one specific widget rather than whatever happens to be painted behind a region. `ImageFiltered` filters its own child, so a blurred background photo is `ImageFiltered(imageFilter: ImageFilter.blur(...), child: Image.asset(...))`; the API docs call this both simpler and much cheaper than a `BackdropFilter` layered over the image.
- Why must frosted surfaces that share a BackdropGroup not overlap?Sharing a backdrop key tells the engine it may read and blur the backdrop once for all of them. Where two grouped filters overlap, the result looks as if only one blur was applied, not two stacked blurs. Keep overlapping filters on separate keys, or plain `BackdropFilter` without a group.
saying these in an interview costs you the question
- BackdropFilter only blurs the area of its own child.
- The stutter must be rebuilds, so const widgets will fix it.
- A RepaintBoundary around the list caches what the blur reads.
- Setting sigma to zero is the proper way to turn the blur off.
- Debug-mode frame times are good enough to judge the blur's cost.