Repository navigation
Transform.rotate is super slow #24627
Copy link
Copy link
Closed
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: performanceRelates to speed or footprint issues (see "perf:" labels)Relates to speed or footprint issues (see "perf:" labels)engineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.19Found to occur in 3.19Found to occur in 3.19found in release: 3.22Found to occur in 3.22Found to occur in 3.22has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Description
I'm implementing a card swiping tinder like ui. Cards are being moved during onPanUpdate (within GestureDetector).
When applying just regular Transform.translate it is super smooth, but with when I apply also Transform.rotate it is missing a lot of frames (even on iPhone X).
This is the sample code:
class MemberCard extends StatelessWidget {
final double width;
final double height;
final String photo;
final double rotation;
final Offset offset;
final double scale;
final bool dropShadow;
MemberCard({
this.width,
this.height,
this.photo,
this.rotation = 0.0,
this.offset = const Offset(0.0, 0.0),
this.scale = 1.0,
this.dropShadow = false,
});
@override
Widget build(BuildContext context) {
double angle = rotation * (math.pi / 180.0);
var matrix = Matrix4.translationValues(offset.dx, offset.dy, 0.0);
if (scale != 1.0) {
matrix..scale(scale);
}
if (rotation != 0.0) {
matrix.rotateZ(angle);
}
return Transform(
alignment: Alignment.center,
transform: matrix,
child: Stack(
children: <Widget>[
RepaintBoundary(
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
boxShadow: dropShadow
? [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 5.0,
offset: Offset(0.0, 1.0))
]
: [],
borderRadius: BorderRadius.all(Radius.circular(10.0)),
color: Colors.white,
// image: DecorationImage(
// image: NetworkImage(photo),
// fit: BoxFit.cover,
// ),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Image.network(
photo,
fit: BoxFit.cover,
filterQuality: FilterQuality.none,
),
),
),
),
],
),
);
}
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: performanceRelates to speed or footprint issues (see "perf:" labels)Relates to speed or footprint issues (see "perf:" labels)engineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.19Found to occur in 3.19Found to occur in 3.19found in release: 3.22Found to occur in 3.22Found to occur in 3.22has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Activity
arsen commentedon Nov 21, 2018
Output of flutter doctor -v
GaryQian commentedon Dec 6, 2018
cc @liyuqian
liyuqian commentedon Dec 7, 2018
Rotation is much slower than translation because we can raster cache during translation, but we can't do it during rotation in order to be pixel perfect.
For example, an axis-aligned rectangle may not have any anti-aliased pixels, while a slightly rotated rectangle may have tons of anti-aliased pixels as shown below. Therefore we can't reuse the cached pixels with rotation.
slightly rotated rectangle

the anti-aliased pixels of the slightly rotated rectangle:

In your specific case, I guess that the shadow is an expensive operation and we currently can't draw a big shadow at 60fps without caching. A solution might be for you to do a snapshot of your shadowed image and rotate that snapshot every frame so Flutter won't try to paint an expensive but pixel-perfect shadow every frame.
Another possibility is for us to add an option to RepaintBoundary which enables raster cache during rotation with a sacrifice in fidelity. @Hixie @chinmaygarde : what's your thought on adding that option to RepaintBoundary? (FYI, We will add raster cache option to RepaintBoundary for retained rendering.)
arsen commentedon Dec 12, 2018
Hi @liyuqian,
Thank you for explaining in details.
I have a quick question, what do you mean by "do a snapshot of your shadowed image"?
Do you mean create a separate widget and wrap in RepaintBoundary?
liyuqian commentedon Dec 13, 2018
Hi @arsen , I'm referring to the code here: https://gist.github.com/liyuqian/5b9eed3d9d80dde0c93da84d4c45aa34
Using my Moto G4 test device,
useSnapshot = true, I get an average frame render time of ~4msuseSnapshot = false, I get an average frame render time of ~18ms.arsen commentedon Dec 26, 2018
Hi @liyuqian,
Thanks for example code. This is really really helpful.
I have implemented the same approach, but I do see a flicker when it is switching to a snapshot version.
Even in your example, this flicker is noticeable.
I have tried putting them on top of each other (in a stack) and hide the original version (with a delay) once a snapshot is ready, but it is not as smooth also (boxShadow gets darker because of two containers with the same shadow).
Any thoughts?
Thanks.
liyuqian commentedon Dec 26, 2018
The flicker is due to sending the image from CPU memory to GPU memory. I modified your
Stackhack to wait for 1 second to populate the GPU memory before showing the snapshot. I used a white Container in the middle of the stack to completely occlude the snapshot when it's not ready to avoid the flicker and the double shadow: https://gist.github.com/liyuqian/5b9eed3d9d80dde0c93da84d4c45aa34 (check_gpuImageCachedandcacheStack).But this might be too much of a hack. @chinmaygarde, @jason-simmons, @cbracken : is there a more elegant way to tell a widget to show an image only when that image is in the GPU memory?
Hixie commentedon Jan 4, 2019
We could have a flag on RepaintBoundary that says to cache the bitmap when we first paint it and then reuse that even if the transform changes? That seems cleaner than having people send pixels from the GPU to the CPU and back to the GPU.
Even better would be to make this scene something that can actually paint at a reasonable speed.
arsen commentedon Jan 9, 2019
Hi @Hixie ,
That would be really great :)
Should I wait for it? how hard will it be to implement?
Hixie commentedon Jan 15, 2019
I wouldn't recommend waiting for it, nobody is currently signed up to do it. :-)
Not sure how easy it would be to implement either the "original bitmap cache" idea or the "just make it faster" solution. @liyuqian may be able to advise us.
liyuqian commentedon Jan 15, 2019
Yes, don't wait for it. Although I really want to implement the best solution for it ASAP, I have many other tasks piled up... My really aggressive goal is to have it done by Q1, but maybe a safer bet is Q2.
liyuqian commentedon Jun 26, 2019
@flar : since you're the assignee of #32662 which will fix this issue, can I also assign you to this issue? BTW, it would be nice to add the reproduction code here to our benchmarks :)
20 remaining items
flutter-triage-bot commentedon Jul 8, 2023
This issue is assigned but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!
flutter-triage-bot commentedon Dec 4, 2023
This issue is assigned to @flar but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!
huycozy commentedon Apr 12, 2024
Re-checking the issue on the latest Flutter version. On iOS (with and without Impeller, it looks better, sometimes I see jank but not much). But on Android (Skia), I can observe a lot of janks there.
Recorded video - Android device - profile mode
Screen.Recording.2024-04-12.at.16.16.13.mov
Sample code (be compatible with the new flutter version)
flutter doctor -v (stable and master)
[✓] Flutter (Channel stable, 3.19.5, on macOS 14.1 23B74 darwin-x64, locale en-VN) • Flutter version 3.19.5 on channel stable at /Users/huynq/Documents/GitHub/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 300451adae (8 days ago), 2024-03-27 21:54:07 -0500 • Engine revision e76c956498 • Dart version 3.3.3 • DevTools version 2.31.1 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/huynq/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = /Users/huynq/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.2) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 15C500b • CocoaPods version 1.15.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2023.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • android-studio-dir = /Applications/Android Studio.app/ • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874) [✓] VS Code (version 1.87.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.84.0 [✓] Connected device (2 available) • macOS (desktop) • macos • darwin-x64 • macOS 14.1 23B74 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 123.0.6312.106 [✓] Network resources • All expected network resources are available. • No issues found!jonahwilliams commentedon Feb 28, 2025
I'm looking at the difference on a Pixel 7, granted a very new/fast phone. Non-snapshot version:
Skia
Impeller
I'm pretty happy with these results and would like to call this fixed (as Impeller is on by default for both Android/iOS)
github-actions commentedon Mar 14, 2025
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.