In Flutter, why is wrapping a subtree in an Opacity widget with a value like 0.5 costly, and what cheaper options exist?
answer
- work lands on the raster thread
- an intermediate offscreen buffer
- 0.0 and 1.0 take a fast path
- alpha inside the color itself
- Image.opacity takes an Animation
basics
~20 sA fractional Opacity makes Flutter paint the child into an offscreen buffer (a saveLayer) and composite it back, which costs raster time. Cheaper: put the alpha in the color, use Image's color or opacity parameters, or Visibility for on/off.
solid answer
~40 s`Opacity` applies one alpha to a whole group, so for any value strictly between 0.0 and 1.0 the child is usually drawn into an intermediate offscreen buffer and then blended back: a `saveLayer`, paid on the raster thread, with extra memory and a render-target switch. The values 0.0 and 1.0 take a fast path: at 0.0 the child is not painted at all. When only one color or one image needs transparency, draw it translucent directly: `Colors.red.withValues(alpha: 0.5)` on a `ColoredBox`, or `Image`'s `color` with `BlendMode.modulate` or its `opacity` parameter. For plain show/hide use 0.0/1.0 or `Visibility`. Keep `Opacity` for a group whose overlapping parts must fade as one flattened picture.
code
dart · 22 linesimport 'package:flutter/material.dart';
// Costly: a fractional Opacity over a subtree.
Widget costlyBox() => const Opacity(
opacity: 0.5,
child: ColoredBox(
color: Colors.red,
child: SizedBox(width: 80, height: 80),
),
);
// Cheaper: the alpha lives in the paint itself.
Widget cheaperBox() => ColoredBox(
color: Colors.red.withValues(alpha: 0.5),
child: const SizedBox(width: 80, height: 80),
);
// Cheaper for an image: alpha applied while the image is drawn.
Widget fadedPhoto() => Image.asset(
'assets/photo.png',
opacity: const AlwaysStoppedAnimation<double>(0.5),
);go deeper
Remember that a value between 0 and 1 on Opacity is not free, and that a translucent color or Image's own parameters are the cheap route.
Explain the offscreen buffer: draw the subtree, then blend it back once, and why 0.0 and 1.0 skip it. Know the hit-testing trap at 0.0.
Show how you would prove opacity is the raster cost on a real device, and when group opacity is visually required so the buffer is worth paying for.
Frame opacity as a design-system decision: translucent tokens in colors rather than widget-level fades, and a rule for where group effects are allowed.
## What the Opacity widget really does `Opacity` is a single-child widget whose render object, `RenderOpacity`, scales the alpha of **everything its child paints** by one factor. The important word is *everything*: the child may be a whole subtree of text, icons, backgrounds and borders that overlap each other. To make that group look like one semi-transparent picture, Flutter composites it through an **`OpacityLayer`**, and on the raster thread that generally means a **`saveLayer`**: the subtree is first drawn at full strength into an **offscreen buffer**, and the buffer is then blended onto the screen at the requested alpha. The API documentation states the rule plainly: values **1.0 and 0.0 are painted with a fast path**, while other values require painting the child into an intermediate buffer, which is expensive. ## Why an offscreen buffer costs so much - **Allocation.** The buffer is a texture the size of the child's bounds; big subtrees mean big buffers. - **Render-target switches.** The GPU must stop drawing to the screen, draw into the buffer, then switch back. The Flutter docs describe the GPU as wanting to "run like a firehose" and note these switches are particularly slow on older GPUs. - **Extra blending pass.** The buffer is composited back as one more full-area draw. - **It is invisible in the build.** Nothing on the UI thread looks slow, so the jank only shows as long **raster** times. The engine can sometimes fold the alpha straight into the child's drawing when the child's operations do not overlap (a single rectangle, for example), but you cannot rely on that for a general subtree. ## The fast paths and what they mean | Opacity value | What happens | |---|---| | `0.0` | Child is not painted; it still takes up layout space and still receives hit tests | | strictly between 0 and 1 | Child is usually drawn through an offscreen buffer | | `1.0` | Treated as opaque; no intermediate buffer is needed | The hit-testing detail matters: an invisible child under `Opacity(opacity: 0.0)` can still be tapped, so pair it with `IgnorePointer`, or use `Visibility`, whose `Visibility.maintain` constructor is documented as equivalent to an opacity of 0.0 or 1.0. ## Cheaper ways to get the same pixels 1. **Put the alpha in the color.** `Container(color: Color.fromRGBO(255, 0, 0, 0.5))` is documented as much faster than `Opacity(opacity: 0.5, child: Container(color: Colors.red))`. In current Flutter write the color as `Colors.red.withValues(alpha: 0.5)`; `Color.withOpacity` has been deprecated since 3.27. 2. **Let the image apply it.** `Image` accepts a `color` plus `colorBlendMode: BlendMode.modulate`, and an `opacity` parameter that takes an `Animation<double>` (use `AlwaysStoppedAnimation(0.5)` for a fixed value). Its documentation says this avoids creating a new composited layer. 3. **Fade images in with `FadeInImage`**, which the performance guide recommends over wrapping an image in `Opacity`. 4. **Apply alpha to individual leaves** rather than to a parent, when those leaves do not overlap, so no group buffer is needed. 5. **Use 0.0/1.0 or `Visibility`** when the real requirement is shown versus hidden. ## When Opacity is still the right tool Group opacity is not a bug; it produces a different picture. If a translucent card contains an avatar overlapping a badge, per-child alpha would let the avatar show through the badge, while group opacity flattens the card first so the overlap stays hidden. When that look is required, `Opacity` (or an animated fade widget) is correct: keep its subtree small and do not stack several of them. ## Checking the cost Profile on a real device in profile mode and compare raster times with and without the effect. In debug builds the DevTools Performance page's **Render Opacity layers** switch (the `debugDisableOpacityLayers` flag) skips opacity layers when building the scene; if raster time falls sharply with it off, opacity is part of the problem. Debug timings are not representative, so use the switch as a relative signal only.
- In Flutter, can a user still tap a button hidden under Opacity(opacity: 0.0)?Yes. `Opacity` at 0.0 skips painting the child but does not change hit testing, so the invisible button still receives taps. Wrap it in `IgnorePointer`, or use `Visibility` (which can also drop the child from layout and interaction), when hidden must also mean inert.
- Why does 50% opacity on a parent look different from giving each child 50% alpha?Group opacity flattens the subtree first and fades the result once, so overlapping children do not show through each other. Per-child alpha fades each paint separately, so overlaps blend and look darker or show hidden edges. The flattening is exactly what the offscreen buffer buys, which is why the cheaper per-child route only fits non-overlapping content.
Tinting a glass pane is cheap; fading a whole collage evenly means photographing the collage first and then printing the photo at half strength. Opacity is the photograph step.
saying these in an interview costs you the question
- Opacity just multiplies pixel alpha, so any value is free.
- Opacity at 0.0 also makes the child ignore taps.
- Fading a parent to 0.5 always looks identical to fading each child to 0.5.
- Color.withOpacity is the current way to put alpha in a color.
- A slow Opacity will show up as long UI-thread build times.