Skip to content

[Windows][Impeller] Scrolling basic LinearGradient decorations causes severe raster slowdown #190401

Open

Description

@Razorex001

Summary

On Windows, with Impeller, scrolling a scene containing very basic two-color LinearGradient decorations causes a severe and sustained raster slowdown.

The Code Sample that I provided measures two phases automatically:

  • Rectangles painted with BoxDecoration.color
  • Rectangles painted with a two-color LinearGradient

With Impeller, the gradient phase has a raster p50 of 20.432 ms, and 80.7% of its raster frames exceed the 60 Hz frame budget. With Skia, the same gradient phase has a raster p50 of 1.396 ms, with no frames over budget.

This mini Flutter app compares Windows rendering performance when scrolling identical rectangles painted with a solid color and then with a basic two-color linear gradient. It automatically runs both scenarios at a fixed scroll speed, and reports UI/raster frame-time percentiles, FPS, and missed-frame counts in the console. The app uses Flutter's FrameTiming API. It displays the results in the application and prints the same summary to the terminal. It also emits these timeline events for optional DevTools verification:

  • GradientRepro_SolidColor
  • GradientRepro_LinearGradient

Steps to reproduce

  1. Create a new Flutter project.
  2. Replace lib/main.dart with the provided code sample.
  3. Run the Windows application in profile mode using Impeller:
flutter run -d windows --profile
  1. Maximize the application window.
  2. Press F10 or click Run comparison.
  3. Do not interact with the application until both phases finish.
  4. Repeat the test using Skia:
flutter run -d windows --profile --no-enable-impeller

Expected results

Using a basic two-color linear gradient should have a reasonably similar raster cost on Impeller and Skia. It should not cause most frames to exceed the frame budget or reduce scrolling below the display refresh rate.

Actual results

Tests were performed using a maximized 1920×1017 physical viewport, DPR 1, a 60 Hz display, and deterministic scrolling at 1400 logical pixels per second.

Renderer Scenario Frames Observed FPS UI p50 / p95 Raster p50 / p90 / p95 / p99 / max Raster over budget
Impeller Solid color 239 59.50 0.547 / 0.597 ms 1.492 / 1.596 / 1.632 / 1.670 / 1.804 ms 0/239 (0%)
Impeller Linear gradient 181 45.00 1.188 / 2.134 ms 20.432 / 29.767 / 31.727 / 35.823 / 44.162 ms 146/181 (80.7%)
Skia Solid color 239 59.50 0.564 / 0.618 ms 0.860 / 0.978 / 1.035 / 1.239 / 1.353 ms 0/239 (0%)
Skia Linear gradient 239 59.50 1.070 / 1.174 ms 1.396 / 1.545 / 1.609 / 1.809 / 2.249 ms 0/239 (0%)

At raster p50:

  • Impeller's linear-gradient phase is approximately 13.7× slower than the solid-color phase.
  • Impeller's linear-gradient phase is approximately 14.6× slower than it is for the same phase on Skia.
  • The Impeller gradient phase exceeds the raster budget in 80.7% of measured frames.
  • Skia completes both phases at 59.5 FPS without exceeding the raster budget.

The UI timings remain relatively low. The primary difference is in raster duration.

Code sample

Code sample
import 'dart:async';
import 'dart:developer';
import 'dart:math' as math;
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

void main() => runApp(const ReproductionApp());

class ReproductionApp extends StatefulWidget {
  const ReproductionApp({super.key});

  @override
  State<ReproductionApp> createState() => _ReproductionAppState();
}

class _ReproductionAppState extends State<ReproductionApp> {
  bool _showPerformanceOverlay = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      showPerformanceOverlay: _showPerformanceOverlay,
      theme: ThemeData.dark(useMaterial3: true),
      home: ReproductionScreen(
        showPerformanceOverlay: _showPerformanceOverlay,
        onPerformanceOverlayChanged: (bool value) {
          setState(() => _showPerformanceOverlay = value);
        },
      ),
    );
  }
}

class ReproductionScreen extends StatefulWidget {
  const ReproductionScreen({
    super.key,
    required this.showPerformanceOverlay,
    required this.onPerformanceOverlayChanged,
  });

  final bool showPerformanceOverlay;
  final ValueChanged<bool> onPerformanceOverlayChanged;

  @override
  State<ReproductionScreen> createState() => _ReproductionScreenState();
}

class _ReproductionScreenState extends State<ReproductionScreen>
    with TickerProviderStateMixin {
  final ScrollController _scrollController = ScrollController();
  late final FrameComparisonRunner _runner;
  bool _useGradient = false;
  bool _running = false;
  String _status = 'Maximize the window, then run the comparison.';
  List<ComparisonResult>? _results;

  @override
  void initState() {
    super.initState();
    _runner = FrameComparisonRunner(vsync: this, controller: _scrollController);
  }

  @override
  void dispose() {
    _runner.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  Future<void> _runComparison() async {
    if (_running) {
      return;
    }
    if (!_scrollController.hasClients ||
        !_scrollController.position.hasContentDimensions ||
        _scrollController.position.maxScrollExtent <= 0) {
      setState(() => _status = 'The scroll view is not ready. Try again.');
      return;
    }

    setState(() {
      _running = true;
      _results = null;
      _status =
          'Running automatically: solid color, 1-second pause, then gradient.';
    });
    try {
      final List<ComparisonResult> results = await _runner.run(
        view: View.of(context),
        onGradientChanged: (bool enabled) {
          if (mounted) {
            setState(() => _useGradient = enabled);
          }
        },
      );
      if (mounted) {
        setState(() {
          _results = results;
          _status = 'Comparison complete. Results are shown below.';
        });
      }
    } catch (error, stackTrace) {
      debugPrint('Comparison failed: $error\n$stackTrace');
      if (mounted) {
        setState(() => _status = 'Comparison failed: $error');
      }
    } finally {
      _running = false;
      if (mounted) {
        setState(() {});
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return CallbackShortcuts(
      bindings: <ShortcutActivator, VoidCallback>{
        const SingleActivator(LogicalKeyboardKey.f10): _runComparison,
      },
      child: Focus(
        autofocus: true,
        child: Scaffold(
          backgroundColor: const Color(0xff080808),
          body: Column(
            children: <Widget>[
              SizedBox(height: 190, child: _buildControls()),
              Expanded(
                child: _GradientScrollScene(
                  controller: _scrollController,
                  useGradient: _useGradient,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildControls() {
    return ColoredBox(
      color: const Color(0xff202020),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Row(
              children: <Widget>[
                const Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        'Windows linear-gradient comparison',
                        style: TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      Text(
                        'Automatic solid-color / gradient comparison',
                        style: TextStyle(color: Colors.white60, fontSize: 12),
                      ),
                    ],
                  ),
                ),
                Text(
                  kProfileMode ? 'PROFILE MODE' : 'NOT PROFILE',
                  style: TextStyle(
                    color: kProfileMode
                        ? const Color(0xff65e6a8)
                        : const Color(0xffffad66),
                    fontWeight: FontWeight.bold,
                    fontSize: 12,
                  ),
                ),
                const SizedBox(width: 16),
                const Text('Performance overlay'),
                Switch(
                  value: widget.showPerformanceOverlay,
                  onChanged: _running
                      ? null
                      : widget.onPerformanceOverlayChanged,
                ),
                const SizedBox(width: 12),
                FilledButton(
                  onPressed: _running ? null : _runComparison,
                  child: const Text('Run comparison (F10)'),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              _status,
              style: const TextStyle(color: Color(0xff79e4ff), fontSize: 12),
            ),
            const SizedBox(height: 8),
            Expanded(child: _buildResults()),
          ],
        ),
      ),
    );
  }

  Widget _buildResults() {
    final List<ComparisonResult>? results = _results;
    if (results == null) {
      return Text(
        kProfileMode
            ? 'Leave the overlay off for the numeric run.'
            : 'Relaunch with --profile before collecting reportable results.',
        style: const TextStyle(color: Colors.white54, fontSize: 12),
      );
    }
    return SelectableText(
      <String>[
        'Mode                 FPS    UI p50/p95       '
            'Raster p50/p95/p99/max                 Over budget',
        ...results.map((ComparisonResult result) => result.displayLine),
      ].join('\n'),
      style: const TextStyle(
        color: Colors.white,
        fontFamily: 'Consolas',
        fontSize: 11,
        height: 1.45,
      ),
    );
  }
}

class _GradientScrollScene extends StatelessWidget {
  const _GradientScrollScene({
    required this.controller,
    required this.useGradient,
  });

  static const int _columnCount = 7;
  static const int _groupsPerColumn = 28;

  final ScrollController controller;
  final bool useGradient;

  @override
  Widget build(BuildContext context) {
    // Seven columns keep the visible workload consistent in both phases.
    return ScrollConfiguration(
      behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
      child: SingleChildScrollView(
        controller: controller,
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: List<Widget>.generate(_columnCount, (int column) {
            return Expanded(
              child: Padding(
                padding: const EdgeInsets.all(6),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: List<Widget>.generate(_groupsPerColumn, (int row) {
                    return _GradientCell(useGradient: useGradient);
                  }),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }
}

class _GradientCell extends StatelessWidget {
  const _GradientCell({required this.useGradient});

  final bool useGradient;

  @override
  Widget build(BuildContext context) {
    final Decoration decoration = useGradient
        ? const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
              colors: <Color>[Color(0xff164d66), Color(0xff7b285d)],
            ),
          )
        : const BoxDecoration(color: Color(0xff164d66));

    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: SizedBox(height: 230, child: DecoratedBox(decoration: decoration)),
    );
  }
}

/// Runs the solid-color/gradient sequence and summarizes Flutter's engine-reported
/// [FrameTiming] values for each measurement window.
class FrameComparisonRunner {
  FrameComparisonRunner({required this._vsync, required this._controller});

  static const double _scrollSpeed = 1400;
  static const Duration _unmeasuredScrollDuration = Duration(seconds: 2);
  static const Duration _measurementDuration = Duration(seconds: 4);
  static const Duration _timingDrainDuration = Duration(seconds: 1);
  static const Duration _interPhasePause = Duration(seconds: 1);
  static const double _maximumDeltaSeconds = 1 / 30;

  final TickerProvider _vsync;
  final ScrollController _controller;
  Ticker? _activeTicker;
  bool _running = false;
  bool _disposed = false;

  Future<List<ComparisonResult>> run({
    required FlutterView view,
    required ValueChanged<bool> onGradientChanged,
  }) async {
    if (_running || _disposed) {
      return const <ComparisonResult>[];
    }

    _running = true;
    try {
      final ComparisonResult solidColor = await _runPhase(
        view: view,
        label: 'Solid color',
        timelineLabel: 'GradientRepro_SolidColor',
        useGradient: false,
        onGradientChanged: onGradientChanged,
      );

      _controller.jumpTo(0);
      await Future<void>.delayed(_interPhasePause);

      final ComparisonResult gradient = await _runPhase(
        view: view,
        label: 'Linear gradient',
        timelineLabel: 'GradientRepro_LinearGradient',
        useGradient: true,
        onGradientChanged: onGradientChanged,
      );
      final List<ComparisonResult> results = <ComparisonResult>[
        solidColor,
        gradient,
      ];
      _printResults(results);
      return results;
    } finally {
      _running = false;
    }
  }

  Future<ComparisonResult> _runPhase({
    required FlutterView view,
    required String label,
    required String timelineLabel,
    required bool useGradient,
    required ValueChanged<bool> onGradientChanged,
  }) async {
    onGradientChanged(useGradient);
    await SchedulerBinding.instance.endOfFrame;

    final double extent = _controller.position.maxScrollExtent;
    _controller.jumpTo(0);
    await SchedulerBinding.instance.endOfFrame;
    await _drive(duration: _unmeasuredScrollDuration, extent: extent);

    _controller.jumpTo(0);
    await SchedulerBinding.instance.endOfFrame;

    final List<FrameTiming> timings = <FrameTiming>[];
    int? startMicros;
    int? endMicros;
    void collect(List<FrameTiming> batch) => timings.addAll(batch);

    SchedulerBinding.instance.addTimingsCallback(collect);
    final TimelineTask task = TimelineTask()
      ..start(
        timelineLabel,
        arguments: <String, Object>{'gradient_enabled': useGradient},
      );
    try {
      await _drive(
        duration: _measurementDuration,
        extent: extent,
        onFirstFrame: (int value) => startMicros = value,
        onLastFrame: (int value) => endMicros = value,
      );
      // FrameTiming batches can arrive after their frames have completed.
      await Future<void>.delayed(_timingDrainDuration);
    } finally {
      task.finish();
      SchedulerBinding.instance.removeTimingsCallback(collect);
    }

    // Keep only frames whose build started inside the driven measurement.
    final List<FrameTiming> measurementWindow =
        timings.where((FrameTiming timing) {
          final int timestamp = timing.timestampInMicroseconds(
            FramePhase.buildStart,
          );
          return startMicros != null &&
              endMicros != null &&
              timestamp >= startMicros! &&
              timestamp <= endMicros!;
        }).toList()..sort((FrameTiming a, FrameTiming b) {
          return a
              .timestampInMicroseconds(FramePhase.buildStart)
              .compareTo(b.timestampInMicroseconds(FramePhase.buildStart));
        });

    return ComparisonResult.fromTimings(
      label: label,
      timings: measurementWindow,
      measurementDuration: _measurementDuration,
      refreshRate: view.display.refreshRate,
    );
  }

  /// Moves the scroll offset once per ticker callback. The delta cap prevents
  /// a delayed callback from producing one abnormally large scroll jump.
  Future<void> _drive({
    required Duration duration,
    required double extent,
    ValueChanged<int>? onFirstFrame,
    ValueChanged<int>? onLastFrame,
  }) async {
    final Completer<void> done = Completer<void>();
    Duration? previous;
    double direction = 1;
    bool started = false;

    late final Ticker ticker;
    ticker = _vsync.createTicker((Duration elapsed) {
      final Duration? last = previous;
      previous = elapsed;
      if (last == null) {
        return;
      }

      final double deltaSeconds = math.min(
        (elapsed - last).inMicroseconds / Duration.microsecondsPerSecond,
        _maximumDeltaSeconds,
      );
      final int frameTimestamp =
          SchedulerBinding.instance.currentSystemFrameTimeStamp.inMicroseconds;
      if (!started) {
        started = true;
        onFirstFrame?.call(frameTimestamp);
      }

      double target =
          _controller.offset + _scrollSpeed * direction * deltaSeconds;
      if (target >= extent) {
        target = extent;
        direction = -1;
      } else if (target <= 0) {
        target = 0;
        direction = 1;
      }
      _controller.jumpTo(target);

      if (elapsed >= duration) {
        onLastFrame?.call(frameTimestamp);
        ticker.stop();
        done.complete();
      }
    });
    _activeTicker = ticker;
    ticker.start();
    await done.future;
    ticker.dispose();
    if (identical(_activeTicker, ticker)) {
      _activeTicker = null;
    }
  }

  void _printResults(List<ComparisonResult> results) {
    debugPrint('=== Windows linear-gradient comparison ===');
    debugPrint(
      'mode=${kProfileMode ? 'profile' : 'NOT_PROFILE'} '
      'speed=${_scrollSpeed.round()}px/s',
    );
    for (final ComparisonResult result in results) {
      debugPrint(result.consoleLine);
    }
    debugPrint('================================================');
  }

  void dispose() {
    _disposed = true;
    _activeTicker?.dispose();
    _activeTicker = null;
  }
}

class ComparisonResult {
  const ComparisonResult({
    required this.label,
    required this.frameCount,
    required this.fps,
    required this.build,
    required this.raster,
    required this.rasterOverBudget,
    required this.rasterOverBudgetPercent,
  });

  factory ComparisonResult.fromTimings({
    required String label,
    required List<FrameTiming> timings,
    required Duration measurementDuration,
    required double refreshRate,
  }) {
    final List<int> buildMicros = timings
        .map((FrameTiming timing) => timing.buildDuration.inMicroseconds)
        .toList();
    final List<int> rasterMicros = timings
        .map((FrameTiming timing) => timing.rasterDuration.inMicroseconds)
        .toList();
    final double budgetMicros = Duration.microsecondsPerSecond / refreshRate;
    final int overBudget = rasterMicros
        .where((int value) => value > budgetMicros)
        .length;
    final double seconds =
        measurementDuration.inMicroseconds / Duration.microsecondsPerSecond;

    return ComparisonResult(
      label: label,
      frameCount: timings.length,
      fps: timings.length < 2 ? 0 : (timings.length - 1) / seconds,
      build: TimingSummary.fromMicroseconds(buildMicros),
      raster: TimingSummary.fromMicroseconds(rasterMicros),
      rasterOverBudget: overBudget,
      rasterOverBudgetPercent: timings.isEmpty
          ? 0
          : overBudget * 100 / timings.length,
    );
  }

  final String label;
  final int frameCount;
  final double fps;
  final TimingSummary build;
  final TimingSummary raster;
  final int rasterOverBudget;
  final double rasterOverBudgetPercent;

  String get displayLine =>
      '${label.padRight(20)} '
      '${fps.toStringAsFixed(2).padLeft(6)}  '
      '${build.p50.toStringAsFixed(2).padLeft(5)}/'
      '${build.p95.toStringAsFixed(2).padRight(5)} ms   '
      '${raster.p50.toStringAsFixed(2).padLeft(6)}/'
      '${raster.p95.toStringAsFixed(2)}/'
      '${raster.p99.toStringAsFixed(2)}/'
      '${raster.max.toStringAsFixed(2)} ms   '
      '$rasterOverBudget/$frameCount '
      '(${rasterOverBudgetPercent.toStringAsFixed(1)}%)';

  String get consoleLine =>
      '$label | frames=$frameCount fps=${fps.toStringAsFixed(2)} '
      '| ui p50/p95=${build.p50.toStringAsFixed(3)}/'
      '${build.p95.toStringAsFixed(3)}ms '
      '| raster p50/p90/p95/p99/max=${raster.p50.toStringAsFixed(3)}/'
      '${raster.p90.toStringAsFixed(3)}/'
      '${raster.p95.toStringAsFixed(3)}/'
      '${raster.p99.toStringAsFixed(3)}/'
      '${raster.max.toStringAsFixed(3)}ms '
      '| raster_over_budget=$rasterOverBudget/$frameCount '
      '(${rasterOverBudgetPercent.toStringAsFixed(1)}%)';
}

class TimingSummary {
  const TimingSummary({
    required this.p50,
    required this.p90,
    required this.p95,
    required this.p99,
    required this.max,
  });

  factory TimingSummary.fromMicroseconds(List<int> values) {
    if (values.isEmpty) {
      return const TimingSummary(p50: 0, p90: 0, p95: 0, p99: 0, max: 0);
    }
    final List<int> sorted = List<int>.of(values)..sort();
    return TimingSummary(
      p50: _percentile(sorted, 0.50) / 1000,
      p90: _percentile(sorted, 0.90) / 1000,
      p95: _percentile(sorted, 0.95) / 1000,
      p99: _percentile(sorted, 0.99) / 1000,
      max: sorted.last / 1000,
    );
  }

  final double p50;
  final double p90;
  final double p95;
  final double p99;
  final double max;

  static double _percentile(List<int> sorted, double fraction) {
    final double position = (sorted.length - 1) * fraction;
    final int lower = position.floor();
    final int upper = position.ceil();
    if (lower == upper) {
      return sorted[lower].toDouble();
    }
    return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
  }
}

Performance profiling on master channel

  • The issue still persists on the master channel

Timeline Traces

This was collected when running the app with flutter run -d windows --profile which uses Impeller.

Timeline Traces JSON

DevTools timeline trace

Video demonstration

Video demonstration
test_gradient-IMPELLER.mp4

What target platforms are you seeing this bug on?

Windows

OS/Browser name and version | Device information

Environment

flutter --version
Flutter 3.47.0-1.0.pre-323 • channel master • https://github.com/flutter/flutter.git
Framework • revision 421defc (14 hours ago) • 2026-08-01 09:16:00 -0400
Engine • hash b0fdede3ee25badfe93316fc2c64850866c6913d (revision 421defc) (13 hours ago) • 2026-08-01 13:16:00.000Z
Tools • Dart 3.14.0 (build 3.14.0-82.0.dev) • DevTools 2.61.0-dev.0

  • OS: Windows 10 Pro 22H2, build 19045.7548
  • GPU: Radeon RX 580 Series 4GB
  • GPU driver: 31.0.21923.11000
  • GPU driver date: 2025-07-01 (which is latest one available for my GPU)
  • Display refresh rate: 60 Hz

Does the problem occur on emulator/simulator as well as on physical devices?

Unknown

Is the problem only reproducible with Impeller?

Yes

Flutter Doctor output

Doctor output
flutter doctor -v                                    
[√] Flutter (Channel master, 3.47.0-1.0.pre-323, on Microsoft Windows [Version 10.0.19045.7548], locale it-IT) [2,7s]
    • Flutter version 3.47.0-1.0.pre-323 on channel master at C:\src\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 421defc7a7 (16 hours ago), 2026-08-01 09:16:00 -0400
    • Engine revision 421defc7a7
    • Dart version 3.14.0 (build 3.14.0-82.0.dev)
    • DevTools version 2.61.0-dev.0
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
      enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-windowing, enable-lldb-debugging, enable-uiscene-migration, enable-riscv64

[√] Windows Version (10 Pro 64-bit, 22H2, 2009) [3,0s]

[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [3,2s]
    • Android SDK at C:\Users\moret\AppData\Local\Android\sdk
    • Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
    • Platform android-36.1, build-tools 36.1.0
    • Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)
    • All Android licenses accepted.

[√] Chrome - develop for the web [122ms]
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.14.36) [121ms]
    • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.14.37502.11
    • Windows 10 SDK version 10.0.26100.0

[√] Connected device (3 available) [584ms]
    • Windows (desktop) • windows • windows-x64    • Microsoft Windows [Version 10.0.19045.7548]
    • Chrome (web)      • chrome  • web-javascript • Google Chrome 150.0.7871.187
    • Edge (web)        • edge    • web-javascript • Microsoft Edge 150.0.4078.105

[√] Network resources [686ms]
    • All expected network resources are available.

• No issues found!

Activity

  1. suryaKommana2662 commented on Aug 3, 2026

    @suryaKommana2662
    Contributor

    @Razorex001 Thanks for the detailed report and for providing the reproduction and performance measurements. I was able to reproduce the reported performance regression locally on Windows using the master channel.

    With Impeller enabled:

    • Solid color: raster p50 1.960 ms, 0% frames over budget
    • Linear gradient: raster p50 24.459 ms, 83.9% frames over budget

    With Impeller disabled (Skia):

    • Solid color: raster p50 1.389 ms, 0% frames over budget
    • Linear gradient: raster p50 1.768 ms, 0% frames over budget

    The results show the same pattern described in the report: applying a LinearGradient causes a significant raster performance regression with Impeller, while the corresponding Skia rendering does not show the same slowdown. routing it to the team-engine for further investigation.

  2. added
    engineflutter/engine related. See also e: labels.
    platform-windowsBuilding on or for Windows specifically
    e: impellerImpeller rendering backend issues and features requests
    and removed
    in triagePresently being triaged by the triage team
    on Aug 3, 2026
  3. added
    P2Important issues not at the top of the work list
    on Aug 3, 2026
  4. nekwo commented on Oct 30, 2026

    @nekwo

    I hit this in our app too (Flutter 3.47.8, RTX 4090) and opened a frame in RenderDoc to see why. Each gradient-filled shape gets drawn through its own little chain of five offscreen passes: a white coverage mask, a copy, the gradient, another copy, then a composite back into the main pass. Solid fills and strokes don't do this, which lines up with the solid vs gradient numbers here.

    Skia draws the same gradient in one pass, so with a lot of gradients on screen the extra passes add up fast. Possibly related to #192994 (a similar 3-pass path for other fills) and #193162 (no SSBO support on the GLES backend). Screenshot of the pass list below.

    Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work liste: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.from: performance templateIssues created via a performance issue templateplatform-windowsBuilding on or for Windows specificallyteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions