fix performance issues

This commit is contained in:
Fabian Freund
2026-06-02 03:38:26 +02:00
parent 4172f57a4b
commit 21d8908ef3
2 changed files with 76 additions and 37 deletions
@@ -18,6 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart' import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'
show MdiIcons; 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 { class TopSitesSection extends HookConsumerWidget {
final void Function(Uri uri) onUriSelected; final void Function(Uri uri) onUriSelected;
@@ -156,27 +195,17 @@ class _TopSitesGrid extends ConsumerWidget {
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
sliver: SliverLayoutBuilder( sliver: SliverGrid.builder(
builder: (context, constraints) { gridDelegate: const _TopSitesGridDelegate(),
final layout = _resolveGridLayout(constraints.crossAxisExtent); itemCount: displayItems.length,
return SliverGrid.builder( itemBuilder: (context, index) {
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( final item = displayItems[index];
crossAxisCount: layout.crossAxisCount, return _TopSiteGridTile(
mainAxisSpacing: _gridMainAxisSpacing, item: item,
crossAxisSpacing: _gridCrossAxisSpacing, onTap: () => onUriSelected(item.url),
childAspectRatio: layout.childAspectRatio, onPin: () => _pinItem(context, ref, item),
), onEdit: () => _editItem(context, ref, item),
itemCount: displayItems.length, onRemove: () => _removeItem(context, ref, item),
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),
);
},
); );
}, },
), ),
@@ -84,6 +84,24 @@ class ContainerColors {
static const double surfaceHighAlpha = 0.28; static const double surfaceHighAlpha = 0.28;
static const double outlineBorderAlpha = 0.5; 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( static ContainerColorPalette palette(
BuildContext context, BuildContext context,
Color color, { Color color, {
@@ -93,30 +111,22 @@ class ContainerColors {
final appScheme = theme.colorScheme; final appScheme = theme.colorScheme;
final fullColor = fullOpacity(color); final fullColor = fullOpacity(color);
final seedScheme = useCustomColor
? null
: _seedScheme(fullColor, theme.brightness);
final containerColor = useCustomColor final containerColor = useCustomColor
? fullColor ? fullColor
: ColorScheme.fromSeed( : seedScheme!.primaryContainer;
seedColor: fullColor,
brightness: theme.brightness,
).primaryContainer;
final accentColor = useCustomColor final accentColor = useCustomColor
? _shiftTone(fullColor, theme.brightness) ? _shiftTone(fullColor, theme.brightness)
: ColorScheme.fromSeed( : seedScheme!.primary;
seedColor: fullColor,
brightness: theme.brightness,
).primary;
final onContainerColor = useCustomColor final onContainerColor = useCustomColor
? _contrastingForeground(containerColor) ? _contrastingForeground(containerColor)
: ColorScheme.fromSeed( : seedScheme!.onPrimaryContainer;
seedColor: fullColor,
brightness: theme.brightness,
).onPrimaryContainer;
final onAccentColor = useCustomColor final onAccentColor = useCustomColor
? _contrastingForeground(accentColor) ? _contrastingForeground(accentColor)
: ColorScheme.fromSeed( : seedScheme!.onPrimary;
seedColor: fullColor,
brightness: theme.brightness,
).onPrimary;
final surfaceColor = Color.alphaBlend( final surfaceColor = Color.alphaBlend(
containerColor.withValues(alpha: surfaceAlpha), containerColor.withValues(alpha: surfaceAlpha),