From 21d8908ef3ee9e27a751108e706d4fe685a4ec12 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 2 Jun 2026 03:38:26 +0200 Subject: [PATCH] fix performance issues --- .../empty_state/top_sites_section.dart | 71 +++++++++++++------ .../features/tabs/utils/container_colors.dart | 42 ++++++----- 2 files changed, 76 insertions(+), 37 deletions(-) diff --git a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart index f3a28d31..9a1ddb4e 100644 --- a/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart +++ b/apps/weblibre/lib/features/geckoview/features/search/presentation/widgets/empty_state/top_sites_section.dart @@ -18,6 +18,7 @@ * along with this program. If not, see . */ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart' show MdiIcons; @@ -68,6 +69,44 @@ _TopSitesGridLayout _resolveGridLayout(double width) { ); } +/// Responsive grid delegate that resolves the column count from the available +/// cross-axis extent at the *render* layer (in [getLayout]), mirroring +/// [_resolveGridLayout]. +/// +/// This replaces a `SliverLayoutBuilder`, whose builder re-runs (and rebuilds +/// the entire grid subtree) on every [SliverConstraints] change — i.e. on every +/// scroll and every keyboard-inset animation frame — which rebuilt all +/// top-site tiles each frame. The delegate recomputes grid metrics on layout +/// without rebuilding any widgets. +class _TopSitesGridDelegate extends SliverGridDelegate { + const _TopSitesGridDelegate(); + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + final layout = _resolveGridLayout(constraints.crossAxisExtent); + final crossAxisCount = layout.crossAxisCount; + + final usableCrossAxisExtent = + (constraints.crossAxisExtent - + _gridCrossAxisSpacing * (crossAxisCount - 1)) + .clamp(0.0, double.infinity); + final childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; + final childMainAxisExtent = childCrossAxisExtent / layout.childAspectRatio; + + return SliverGridRegularTileLayout( + crossAxisCount: crossAxisCount, + mainAxisStride: childMainAxisExtent + _gridMainAxisSpacing, + crossAxisStride: childCrossAxisExtent + _gridCrossAxisSpacing, + childMainAxisExtent: childMainAxisExtent, + childCrossAxisExtent: childCrossAxisExtent, + reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), + ); + } + + @override + bool shouldRelayout(_TopSitesGridDelegate oldDelegate) => false; +} + class TopSitesSection extends HookConsumerWidget { final void Function(Uri uri) onUriSelected; @@ -156,27 +195,17 @@ class _TopSitesGrid extends ConsumerWidget { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), - sliver: SliverLayoutBuilder( - builder: (context, constraints) { - final layout = _resolveGridLayout(constraints.crossAxisExtent); - return SliverGrid.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: layout.crossAxisCount, - mainAxisSpacing: _gridMainAxisSpacing, - crossAxisSpacing: _gridCrossAxisSpacing, - childAspectRatio: layout.childAspectRatio, - ), - itemCount: displayItems.length, - itemBuilder: (context, index) { - final item = displayItems[index]; - return _TopSiteGridTile( - item: item, - onTap: () => onUriSelected(item.url), - onPin: () => _pinItem(context, ref, item), - onEdit: () => _editItem(context, ref, item), - onRemove: () => _removeItem(context, ref, item), - ); - }, + sliver: SliverGrid.builder( + gridDelegate: const _TopSitesGridDelegate(), + itemCount: displayItems.length, + itemBuilder: (context, index) { + final item = displayItems[index]; + return _TopSiteGridTile( + item: item, + onTap: () => onUriSelected(item.url), + onPin: () => _pinItem(context, ref, item), + onEdit: () => _editItem(context, ref, item), + onRemove: () => _removeItem(context, ref, item), ); }, ), diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/utils/container_colors.dart b/apps/weblibre/lib/features/geckoview/features/tabs/utils/container_colors.dart index f21625c9..053ac161 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/utils/container_colors.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/utils/container_colors.dart @@ -84,6 +84,24 @@ class ContainerColors { static const double surfaceHighAlpha = 0.28; static const double outlineBorderAlpha = 0.5; + /// Cache of seed schemes keyed by `(seed color, brightness)`. + /// + /// [ColorScheme.fromSeed] runs the full HCT tonal-palette solver, which is + /// very expensive (the `material_color_utilities` `HctSolver`/`DynamicColor` + /// math dominated CPU profiles at hundreds of ms). The result is a pure + /// function of its seed colour and brightness, so generating it once per + /// distinct `(color, brightness)` and reusing it across rebuilds removes the + /// cost entirely — previously seed mode computed a *full* scheme up to four + /// times per [palette] call, for every chip, on every rebuild. + static final Map<(int, Brightness), ColorScheme> _seedSchemeCache = {}; + + static ColorScheme _seedScheme(Color seedColor, Brightness brightness) { + return _seedSchemeCache.putIfAbsent( + (seedColor.toARGB32(), brightness), + () => ColorScheme.fromSeed(seedColor: seedColor, brightness: brightness), + ); + } + static ContainerColorPalette palette( BuildContext context, Color color, { @@ -93,30 +111,22 @@ class ContainerColors { final appScheme = theme.colorScheme; final fullColor = fullOpacity(color); + final seedScheme = useCustomColor + ? null + : _seedScheme(fullColor, theme.brightness); + final containerColor = useCustomColor ? fullColor - : ColorScheme.fromSeed( - seedColor: fullColor, - brightness: theme.brightness, - ).primaryContainer; + : seedScheme!.primaryContainer; final accentColor = useCustomColor ? _shiftTone(fullColor, theme.brightness) - : ColorScheme.fromSeed( - seedColor: fullColor, - brightness: theme.brightness, - ).primary; + : seedScheme!.primary; final onContainerColor = useCustomColor ? _contrastingForeground(containerColor) - : ColorScheme.fromSeed( - seedColor: fullColor, - brightness: theme.brightness, - ).onPrimaryContainer; + : seedScheme!.onPrimaryContainer; final onAccentColor = useCustomColor ? _contrastingForeground(accentColor) - : ColorScheme.fromSeed( - seedColor: fullColor, - brightness: theme.brightness, - ).onPrimary; + : seedScheme!.onPrimary; final surfaceColor = Color.alphaBlend( containerColor.withValues(alpha: surfaceAlpha),