Why are IntrinsicHeight and IntrinsicWidth considered expensive in Flutter, and when is using one still justified?
answer
- speculative pass before layout
- asks the subtree its natural size
- O(N squared) when nested
- LayoutBuilder and viewports cannot answer
- skipped if already tight
basics
~20 sThey ask the whole subtree for its intrinsic size before real layout, a speculative pass that walks the subtree again and can become O(N²) in tree depth when nested. They are fine for a small, shallow subtree, such as one row of equal-height cards.
solid answer
~50 s`IntrinsicHeight` calls `getMaxIntrinsicHeight` on its child for the available width, then lays the child out with that height made tight. Answering the intrinsic query means each render object asks **its** children, so the subtree is walked once speculatively and then again for real layout. Put intrinsic widgets inside each other, or inside something that also queries intrinsics, and the docs warn the cost can reach **O(N²) in the depth of the tree**. Results are cached per render object until its layout is invalidated. Some widgets cannot answer at all: `LayoutBuilder` and scrolling viewports throw. If the height is already tight, `IntrinsicHeight` skips the query. I use it for a single, shallow row — say a lecture page with speaker and summary cards separated by a `VerticalDivider` that must match the taller card — and avoid it in list items or deep trees, preferring fixed heights or `CrossAxisAlignment.stretch` under a bounded parent.
code
dart · 22 linesimport 'package:flutter/material.dart';
class LectureInfoRow extends StatelessWidget {
const LectureInfoRow({super.key, required this.speaker, required this.summary});
final Widget speaker;
final Widget summary;
@override
Widget build(BuildContext context) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(child: Card(child: speaker)),
const VerticalDivider(width: 16),
Expanded(child: Card(child: summary)),
],
),
);
}
}go deeper
Know that IntrinsicHeight makes a Row's children as tall as the tallest child and that it is marked as expensive.
Explain the intrinsic queries, the speculative pass before layout and the stretch-in-a-Row pattern.
Quantify the cost: O(N²) under nesting, caching, the tight-constraint shortcut, widgets that throw, and cheaper alternatives in hot paths.
Set a review rule that intrinsic widgets stay out of list items and deep trees, and require profiling before and after replacements.
## What intrinsic dimensions are Normal Flutter layout is **one pass**: constraints go down, sizes come up. Occasionally a parent needs to know how big a child *would like* to be before it decides the child's constraints. For that, every `RenderBox` can answer four queries: - `getMinIntrinsicWidth(height)` and `getMaxIntrinsicWidth(height)` - `getMinIntrinsicHeight(width)` and `getMaxIntrinsicHeight(width)` For example, `getMaxIntrinsicHeight(300)` means "if you were 300 wide, how tall would you want to be?". ## What the widgets do | Widget | Query | Then | |---|---|---| | `IntrinsicHeight` | child's max intrinsic height for the available width | lays the child out with that height made tight | | `IntrinsicWidth` | child's max intrinsic width | lays the child out with that width made tight, optionally snapped to `stepWidth` / `stepHeight` | The classic use: a `Row` inside `IntrinsicHeight` with `CrossAxisAlignment.stretch`. The `Row` normally has unbounded or loose height; the intrinsic query finds the tallest child, the height becomes tight, and `stretch` makes every child — including a `VerticalDivider` — that tall. ## Why it is expensive 1. **A speculative pass.** To answer the query, the `Row` asks each child, each child asks its children, and so on down the subtree. That walk happens *before* the real layout, which then walks the subtree again. 2. **Nesting multiplies.** If a descendant also calls intrinsic queries during its layout, each level can re-query the levels below. The API docs warn the worst case is **O(N²) in the depth of the tree**. 3. **Repeated in lists.** In a scrolling list, every item built and laid out repeats the work. Mitigations built into the framework: - intrinsic results are **cached** on each render object and cleared when its layout is invalidated; - `IntrinsicHeight` **skips** the query when its incoming height is already tight, and `IntrinsicWidth` does the same for width. ## Widgets that cannot answer Some render objects refuse intrinsic queries and throw: - `LayoutBuilder` — its child depends on constraints that do not exist yet during an intrinsic query; - scrolling viewports behind `ListView`, `GridView` and `CustomScrollView` — answering would mean building every child. So `IntrinsicHeight` around a row that contains a `LayoutBuilder` or a horizontal list fails. ## When it is justified - A **small, shallow** subtree: one row of two or three cards on a lecture detail page. - Built **once per screen**, not once per list item. - No cheaper expression exists: the heights really depend on content, such as translated text of unknown length. ## Alternatives 1. **Fixed or bounded height** plus `CrossAxisAlignment.stretch`: if a `SizedBox(height: 180)` is acceptable, the row stretches its children without any intrinsic query. 2. **Design change**: put the divider inside each card, or use a background instead of a divider, so equal heights are not needed. 3. **A custom render object** that measures children once and sizes itself, when the pattern repeats in a hot path. 4. **Measure before optimising**: profile layout time before replacing an `IntrinsicHeight` that appears once on a screen. ## Summary Intrinsic widgets trade a second, speculative walk of the subtree for content-driven sizing. Explain the query, the O(N²) warning, the widgets that cannot answer, and a concrete case where the cost is acceptable.
- What happens if IntrinsicHeight wraps a Row that contains a LayoutBuilder?The intrinsic query reaches the `LayoutBuilder`, which throws `LayoutBuilder does not support returning intrinsic dimensions`, because its child depends on constraints that are not known during an intrinsic query. Remove the `LayoutBuilder` from that subtree or drop the intrinsic widget.
- Why does IntrinsicHeight add no cost when its parent already gives a tight height?Its render object checks `constraints.hasTightHeight`; if the height is already fixed there is nothing to discover, so it passes the constraints through and never calls `getMaxIntrinsicHeight`.
- Why is IntrinsicHeight inside every item of a long ListView a bigger problem than one on a static page?Each item built or relaid out during scrolling repeats the speculative walk of its subtree. On a static page it happens once; in a list it happens for every item that scrolls into view, adding layout time to frames.
saying these in an interview costs you the question
- IntrinsicHeight is free because it only reads a cached size.
- Every widget can answer intrinsic size queries.
- IntrinsicHeight always queries, even under tight height constraints.
- Wrapping every list item in IntrinsicHeight is a harmless habit.
- Intrinsic widgets make Flutter layout two-pass for the whole app.