perf(mobile): skip unchanged message footer layout (#8433)

Rebuilding an unchanged message footer currently schedules another
layout even when its overlap value is identical. Only invalidate footer
layout when that value changes, preserving layout updates for actual
geometry changes.

### Stable release recommendation

Exclude from `mobile-release/conference-2026-10`; defer to the following
release. The regression test demonstrates eliminated redundant layout,
but the effect on navigation latency has not been quantified.

### Validation

The added widget regression checks that unchanged footer rebuilds do not
repeat layout and changed overlap still updates geometry. The
independent footer build passes the full mobile suite, analysis, and
native iOS Simulator build. Channel scrolling, opening an existing
reaction list, and thread navigation were exercised on iOS 26.5
Simulator.

The full local repository gate previously encountered an unrelated,
unchanged desktop subprocess timing test failure; that test passed in
isolation. All PR CI checks pass, including the native iOS suite.

<!-- buzz-review-completed -->

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-29 14:41:53 -07:00
committed by GitHub
parent c609d33e8c
commit 050d545264
2 changed files with 66 additions and 5 deletions

View File

@@ -54,9 +54,7 @@ class _FooterLayout extends MultiChildRenderObjectWidget {
BuildContext context,
covariant RenderBox renderObject,
) {
(renderObject as _RenderMessageFooter)
..overlap = overlap
..markNeedsLayout();
(renderObject as _RenderMessageFooter).overlap = overlap;
}
}
@@ -70,9 +68,15 @@ class _RenderMessageFooter extends RenderBox
RenderBox,
ContainerBoxParentData<RenderBox>
> {
_RenderMessageFooter(this.overlap);
_RenderMessageFooter(this._overlap);
double overlap;
double _overlap;
double get overlap => _overlap;
set overlap(double value) {
if (_overlap == value) return;
_overlap = value;
markNeedsLayout();
}
@override
void setupParentData(RenderBox child) {

View File

@@ -1,8 +1,47 @@
import 'package:buzz/features/channels/message_footer_layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('unchanged footer geometry does not relayout its timeline row', (
tester,
) async {
var layouts = 0;
final overlap = ValueNotifier(8.0);
addTearDown(overlap.dispose);
await tester.pumpWidget(
MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: _LayoutCounter(
onLayout: () => layouts++,
child: ValueListenableBuilder(
valueListenable: overlap,
builder: (context, value, child) => MessageFooterLayout(
overlap: value,
footer: const SizedBox(width: 100, height: 32),
child: const SizedBox(width: 200, height: 60),
),
),
),
),
),
);
final initialLayouts = layouts;
tester.element(find.byType(MessageFooterLayout)).markNeedsBuild();
await tester.pump();
expect(
layouts,
initialLayouts,
reason: 'Rebuilding unchanged reactions must not dirty the row layout',
);
overlap.value = 12;
await tester.pump();
expect(layouts, greaterThan(initialLayouts));
expect(tester.getSize(find.byType(MessageFooterLayout)).height, 80);
});
testWidgets('footer global transforms and semantics include overlap offset', (
tester,
) async {
@@ -118,3 +157,21 @@ void main() {
},
);
}
class _LayoutCounter extends SingleChildRenderObjectWidget {
const _LayoutCounter({required this.onLayout, required super.child});
final VoidCallback onLayout;
@override
RenderObject createRenderObject(BuildContext context) =>
_CountLayouts(onLayout);
}
class _CountLayouts extends RenderProxyBox {
_CountLayouts(this.onLayout);
final VoidCallback onLayout;
@override
void performLayout() {
onLayout();
super.performLayout();
}
}