Repository: flutter
Issue: Clipping performance regression after update from flutter 3.24.5 to 3.27.1 (Both on skia and impeller)
Labels: platform-android,engine,c: rendering,P3,e: impeller,team-engine,triaged-engine

https://github.com/flutter/flutter/issues/160207#issuecomment-2579218268 in my original issue code sample was too big and it was unclear what was the reason of performance regression, now I found it and creating a new issue since old one contains too many comments.

Devices: Samsung A12 and Pixel 7A.

Inside the AppearanceSettingsPart when i use 
```dart
      clipBehavior: Clip.hardEdge 
      
      /// instead of      
      
       clipBehavior: Clip.antiAliasWithSaveLayer, 
 ```
       
 lag issue becomes so much better, so this might be related to clipping. When i fully remove clipping, app does not lag at all as it was on 3.24.5
 
 
 i did read that clipping (especially with save layer) https://docs.flutter.dev/perf/ui-performance is expensive, but why did it become even more performance-heavy task after update?
 
###  3.27.1:
<img width="1727" alt="image" src="https://github.com/user-attachments/assets/bd1d33cc-c83f-4c9a-bdd0-4abe72012e6c" />

[dart_devtools_2025-01-07_11_14_13.403.json](https://github.com/user-attachments/files/18330922/dart_devtools_2025-01-07_11_14_13.403.json)


### 3.24.5
<img width="1722" alt="image" src="https://github.com/user-attachments/assets/cc76df3e-5f48-46bc-8fc3-e774eeeb7d07" />

[dart_devtools_2025-01-07_11_18_16.457.json](https://github.com/user-attachments/files/18330997/dart_devtools_2025-01-07_11_18_16.457.json)


 ```dart
import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class AppRoutes {
  static const String settings = '/';
  static const String settingsAppearance = '$settings?t=1';
}

void main() {
  runApp(
    MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: appRouter,
    ),
  );
}

final GoRouter appRouter = GoRouter(
  routes: <RouteBase>[
    GoRoute(
      path: AppRoutes.settings,
      pageBuilder: (context, state) {
        return CupertinoPage(
          key: state.pageKey,
          maintainState: false,
          allowSnapshotting: false,
          child: TabControlledScreen(
            tabIndex: int.tryParse('${state.uri.queryParameters['t']}') ?? 0,
            totalTabsAmount: 7,
            routerPath: AppRoutes.settings,
            children: [
              Center(
                child: ButtonWidget(
                  callback: () => appRouter.push(AppRoutes.settingsAppearance),
                  borderRadiusOverride: BorderRadius.zero,
                  iconOrChild: const Text('Open screen'),
                ),
              ),
              const AppearanceSettingsPart(),
            ],
          ),
        );
      },
    ),
  ],
);

class TabControlledScreen extends StatefulWidget {
  const TabControlledScreen({
    super.key,
    required this.tabIndex,
    required this.totalTabsAmount,
    required this.routerPath,
    required this.children,
  });

  final int tabIndex;
  final int totalTabsAmount;
  final String routerPath;
  final List<Widget> children;

  @override
  State<TabControlledScreen> createState() => _TabControlledScreenState();
}

class _TabControlledScreenState extends State<TabControlledScreen>
    with TickerProviderStateMixin {
  late final TabController _tabController = TabController(
    length: widget.totalTabsAmount,
    vsync: this,
    initialIndex: widget.tabIndex.clamp(0, widget.totalTabsAmount - 1),
  )..addListener(_handleTabChanged);

  void _handleTabChanged() {
    setState(() {});
  }

  @override
  void didUpdateWidget(covariant TabControlledScreen oldWidget) {
    if (oldWidget.tabIndex != widget.tabIndex) {
      _tabController.index = widget.tabIndex.clamp(
        0,
        widget.totalTabsAmount - 1,
      );
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: Padding(
        padding: EdgeInsets.only(
          top: max(MediaQuery.viewPaddingOf(context).top, 16.0),
        ),
        child: IndexedStack(
          index: _tabController.index,
          children: widget.children,
        ),
      ),
    );
  }
}

class AppearanceSettingsPart extends StatelessWidget {
  const AppearanceSettingsPart({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12),
        color: Colors.white12,
      ),
      clipBehavior: Clip.antiAliasWithSaveLayer,
      padding: const EdgeInsets.all(24),
      child: Column(
        children: <Widget>[
          const Dropdown(title: 'Theme', initialValue: "PlatformBrightness"),
          const Dropdown(title: 'GrainEffect', initialValue: 'GreatVisible'),
          const Dropdown(
            key: ValueKey(Colors.purple),
            title: 'MainColor',
            initialValue: '0x84329490238',
          ),
          const Dropdown(title: 'FontSize', initialValue: "Standard"),
          const Dropdown(title: 'WeatherSync', initialValue: "Always"),
          const Divider(),
          Switcher(
            initialValue: true,
            onChange: (bool value) async {},
            title: 'BottomMenuBlur',
            description: 'BottomMenuBlurDescription',
          ),
          Switcher(
            initialValue: false,
            onChange: (bool value) async {},
            title: 'DisableAnimations',
            description: 'DisableAnimationsDescription',
          ),
        ],
      ),
    );
  }
}

class ButtonWidget extends StatefulWidget {
  const ButtonWidget({
    super.key,
    this.callback,
    this.iconOrChild,
    this.hitTestBehavior,
    this.borderRadiusOverride,
  });

  final Function? callback;
  final Widget? iconOrChild;
  final HitTestBehavior? hitTestBehavior;
  final BorderRadius? borderRadiusOverride;

  @override
  State<ButtonWidget> createState() => _ButtonWidgetState();
}

class _ButtonWidgetState extends State<ButtonWidget>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(vsync: this);

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<void> _onTapUp(dynamic _) async {
    await Future.delayed(const Duration(milliseconds: 50));
    widget.callback?.call();
  }

  @override
  Widget build(BuildContext context) {
    final Widget childWidget = IgnorePointer(
      ignoring: widget.callback == null,
      child: Padding(
        padding: const EdgeInsets.all(48.0),
        child: widget.iconOrChild,
      ),
    );

    final Widget animationWrapper = childWidget;

    return GestureDetector(
      onTapDown: (_) {

      },
      onTapUp: _onTapUp,
      onTapCancel: () {

      },
      child: animationWrapper,
    );
  }
}

class Dropdown extends StatefulWidget {
  const Dropdown({
    super.key,
    required this.title,
    required this.initialValue,
  });

  final String title;
  final String initialValue;

  @override
  State<Dropdown> createState() => _DropdownState();
}

class _DropdownState extends State<Dropdown> {
  late String selectedValue = widget.initialValue;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.only(bottom: 4),
          child: Text(widget.title),
        ),
        ButtonWidget(
          callback: () {},
          iconOrChild: Container(
            decoration: BoxDecoration(
              color: Colors.purple,
              border: Border.all(
                width: 1.0,
                color: Colors.black,
              ),
              borderRadius: BorderRadius.circular(12),
            ),
            padding: const EdgeInsets.only(
              right: 16,
              left: 16,
            ),
            child: Row(
              children: <Widget>[
                Expanded(
                  child: Text(
                    selectedValue,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

class Switcher extends StatefulWidget {
  const Switcher({
    super.key,
    required this.initialValue,
    required this.onChange,
    required this.title,
    this.description,
  });

  final bool initialValue;
  final Function onChange;
  final String title;
  final String? description;

  @override
  State<Switcher> createState() => _SwitcherState();
}

class _SwitcherState extends State<Switcher> {
  late bool isSelected = widget.initialValue;

  @override
  Widget build(BuildContext context) {
    final Widget container = Container(
      width: 20.0,
      height: 20.0,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(100.0),
        color: Colors.black,
      ),
    );

    return ButtonWidget(
      callback: () {
        setState(() {
          isSelected = !isSelected;
          widget.onChange(isSelected);
        });
      },
      iconOrChild: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: <Widget>[
              Expanded(
                child: Text(
                  widget.title,
                  textAlign: TextAlign.left,
                  maxLines: 3,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              Padding(
                padding: const EdgeInsets.only(left: 12),
                child: _SwitcherAnimatedPart(
                  isSelected: isSelected,
                  child: container,
                ),
              ),
            ],
          ),
          if (widget.description != null)
            Padding(
              padding: const EdgeInsets.only(top: 4),
              child: Text(
                widget.description!,
                textAlign: TextAlign.left,
                maxLines: 3,
                overflow: TextOverflow.ellipsis,
              ),
            ),
        ],
      ),
    );
  }
}

class _SwitcherAnimatedPart extends StatelessWidget {
  const _SwitcherAnimatedPart({
    required this.child,
    required this.isSelected,
  });

  final Widget child;
  final bool isSelected;

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      curve: Curves.easeInOutCubic,
      padding: EdgeInsets.only(
        left: isSelected ? 32 : 4,
        right: !isSelected ? 32 : 4,
        top: 4,
        bottom: 4,
      ),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(100.0),
        color: isSelected ? Colors.white : Colors.grey,
      ),
      child: child,
    );
  }
}


 ```

Visual demo from previous issue with versions comparison:

https://github.com/user-attachments/assets/a4eb5fad-4366-4bae-bd69-65bbe02e8f84



_Originally posted by @krll-kov in https://github.com/flutter/flutter/issues/160207#issuecomment-2574887152_