From 136d3b48c619c0e668243ebe7adfdc40f8dd73b1 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 02:25:38 +0200 Subject: [PATCH] perf(ui): stop the nav-rail brand pulse to fix web scroll jank The DeltaMark ran an endless 9s breathing animation (60fps repaint) inside the persistent nav rail, so it repainted on every scroll frame and stuttered scrolling on Flutter web. Make the rail mark static (animated: false) and isolate the animated variant (landing page) behind a RepaintBoundary. Signed-off-by: flemming-it --- app/lib/pages/shell_page.dart | 6 +++++- app/lib/widgets/delta_mark.dart | 35 ++++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/lib/pages/shell_page.dart b/app/lib/pages/shell_page.dart index 2f7b51a..01aa278 100644 --- a/app/lib/pages/shell_page.dart +++ b/app/lib/pages/shell_page.dart @@ -50,7 +50,11 @@ class _ShellPageState extends State { ), child: Column( children: [ - const DeltaMark(size: 36), + // Static in the persistent nav rail: a 60fps pulse + // here repaints every scroll frame and stutters web + // scrolling. The breathing mark stays on the + // landing page (which isn't scrolled). + const DeltaMark(size: 36, animated: false), const SizedBox(height: ReclaimSpace.sm), const BrandWordmark(size: 18), Text( diff --git a/app/lib/widgets/delta_mark.dart b/app/lib/widgets/delta_mark.dart index fd921cb..6ed7398 100644 --- a/app/lib/widgets/delta_mark.dart +++ b/app/lib/widgets/delta_mark.dart @@ -46,17 +46,30 @@ class _DeltaMarkState extends State @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final t = _controller.value; - // Breathe: scale 1 → 1.28 → 1 over the cycle. - final pulse = 1.0 + 0.28 * (1 - (math.cos(t * 2 * math.pi).abs())); - return CustomPaint( - size: Size.square(widget.size), - painter: _DeltaPainter(pulse: widget.animated ? pulse : 1.0), - ); - }, + // Static mark: no controller listening, paint once. + if (!widget.animated) { + return CustomPaint( + size: Size.square(widget.size), + painter: _DeltaPainter(pulse: 1.0), + ); + } + // Animated: isolate the per-frame repaint in its own layer so + // the breathing pulse never invalidates surrounding content + // (web scroll stays smooth). + return RepaintBoundary( + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final t = _controller.value; + // Breathe: scale 1 → 1.28 → 1 over the cycle. + final pulse = + 1.0 + 0.28 * (1 - (math.cos(t * 2 * math.pi).abs())); + return CustomPaint( + size: Size.square(widget.size), + painter: _DeltaPainter(pulse: pulse), + ); + }, + ), ); } }