Back to Posts

Fixing Animation Jank in Flutter

4 min read

Animation jank can significantly impact user experience in Flutter applications. In this comprehensive guide, we'll explore common causes of animation jank and provide practical solutions to achieve smooth animations.

Understanding Animation Jank

Animation jank occurs when frames are dropped during animation playback, resulting in stuttery or choppy animations. The goal is to maintain 60 frames per second (fps) for smooth animations.

Common Causes and Solutions

1. Heavy Computation on Main Thread

One of the most common causes of jank is performing heavy computations on the main thread.

Problem:

// Bad practice: Heavy computation in animation
AnimationController(
  vsync: this,
  duration: Duration(seconds: 1),
).addListener(() {
  // Heavy computation here
  List<String> items = List.generate(10000, (i) => 'Item $i');
  items.sort();
});

Solution:

// Good practice: Move heavy computation to isolate
AnimationController(
  vsync: this,
  duration: Duration(seconds: 1),
).addListener(() {
  compute(heavyComputation, data);
});

// In a separate function
List<String> heavyComputation(dynamic data) {
  List<String> items = List.generate(10000, (i) => 'Item $i');
  return items..sort();
}

2. Inefficient Widget Rebuilds

Unnecessary widget rebuilds can cause animation jank.

Problem:

// Bad practice: Rebuilding entire tree
class AnimatedWidget extends StatelessWidget {
  final Animation<double> animation;

  AnimatedWidget({required this.animation});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: animation.value * 100,
      child: ExpensiveWidget(), // Rebuilds unnecessarily
    );
  }
}

Solution:

// Good practice: Using AnimatedBuilder
class AnimatedWidget extends StatelessWidget {
  final Animation<double> animation;

  AnimatedWidget({required this.animation});

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return Container(
          width: animation.value * 100,
          child: child,
        );
      },
      child: ExpensiveWidget(), // Built only once
    );
  }
}

3. Complex Animations

Complex animations with multiple simultaneous animations can cause jank.

Solution:

class OptimizedAnimationController extends StatefulWidget {
  @override
  _OptimizedAnimationControllerState createState() => _OptimizedAnimationControllerState();
}

class _OptimizedAnimationControllerState extends State<OptimizedAnimationController>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<double> _rotateAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 500),
      vsync: this,
    );

    // Create multiple animations from single controller
    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.5).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.0, 0.5, curve: Curves.easeOut),
      ),
    );

    _rotateAnimation = Tween<double>(begin: 0.0, end: pi).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.5, 1.0, curve: Curves.easeIn),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnimation.value,
          child: Transform.rotate(
            angle: _rotateAnimation.value,
            child: child,
          ),
        );
      },
      child: YourWidget(), // Built only once
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Debugging Tools

1. Performance Overlay

Enable the performance overlay to monitor frame rendering:

MaterialApp(
  showPerformanceOverlay: true,
  // ... rest of your app
);

2. Timeline Events

Use timeline events to profile your animations:

Timeline.startSync('Animation Start');
// Your animation code
Timeline.finishSync();

Best Practices

  1. Use RepaintBoundary: Isolate frequently animating widgets:
RepaintBoundary(
  child: AnimatedWidget(),
)
  1. Optimize Image Animations:
Image.memory(
  bytes,
  cacheWidth: 300, // Limit image size
  cacheHeight: 300,
)
  1. Use const Widgets: Prevent unnecessary rebuilds:
const StaticWidget(
  child: Text('Static Content'),
)

Conclusion

By following these optimization techniques and best practices, you can significantly reduce animation jank in your Flutter applications. Remember to:

  • Profile your animations using Flutter's performance tools
  • Optimize widget rebuilds
  • Use appropriate animation controllers
  • Move heavy computations off the main thread

Regular testing on various devices will help ensure smooth animations across different platforms and hardware capabilities.