How to avoid jank (laggy animation) during page transition in Flutter

Viewed 840

I have two pages, Page A and Page B. To do transition from Page A to Page B I use Navigation.push():

  Navigator.push(
    context,
    CupertinoPageRoute(...)
  );

However, this transition has so much jank and frame drops. (yes, I run in Profile mode)

One reason I think of is Page B has so much heavy-duty UI rendering (such as Google Maps and Charts) and I also noticed as page slide animation is happening, Page B rendering has already begun.

I'm trying to understand how I can improve this experience and maybe somehow pre-load Page B.

I already read this suggestion from a Github issue (tldr use Future.microtask(()) but it didn't work for me. Would appreciate any help or suggestion.

2 Answers

If the screen transition animation is especially janky the first time you run the app, but then gets smoother if you run the transition back and forth a few times, this is a known problem that both the Flutter team and their counterparts within Android and iOS are working on. They have a suggestion for a workaround here: https://docs.flutter.dev/perf/shader , built on "warming up shaders", but since it only warms up the shaders on the specific device you're debugging on, I honestly feel it's like the programming equivalent of "sweeping it under the rug"... You won't see the problem on your device anymore, but it's still there on other devices!

I however found a workaround for this problem myself, which works surprisingly well! I actually push the janky page, wait a bit, and then pop it again, without the user knowing it! Like this:

import 'login_screen.dart';
import 'register_screen.dart';
import 'home_screen.dart';
import 'loading_screen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';


class WelcomeScreen extends StatefulWidget {
  const WelcomeScreen(Key? key) : super(key: key);

  @override
  _WelcomeScreenState createState() => _WelcomeScreenState();
}

class _WelcomeScreenState extends State<WelcomeScreen> {

  @override
  void initState() {
    super.initState();
    warmUp();
  }

  Future warmUp() async {
    // This silly function is needed to remove jank from the first run screen transition...
    print('Running warmUp()');
    await Firebase.initializeApp();

    if (mounted) {
      Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen(popWhenDone: false)));
      Navigator.push(context, MaterialPageRoute(builder: (context) => RegisterScreen(popWhenDone: false, userType: UserType.artist)));
      Navigator.push(context, MaterialPageRoute(builder: (context) => HomeScreen()));
      Navigator.push(context, MaterialPageRoute(builder: (context) => LoadingScreen()));  // Shows a spinner

      await Future.delayed(Duration(milliseconds: 1000));

      if (mounted) {
        Navigator.popUntil(context, (route) => route.isFirst);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    print('Building $runtimeType');
    return Scaffold(
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                MaterialButton(
                  child: const Text('Sign Up'),
                  onPressed: () {
                    Navigator.push(context, MaterialPageRoute(builder: (context) {
                      return const RegisterScreen();
                    }));
                  },
                ),
                MaterialButton(
                  child: const Text('Log In'),
                  onPressed: () async {
                    await Firebase.initializeApp(); // In case I remove the warmUp() later...
                    if (mounted) {
                      Navigator.push(
                        context,
                        PageRouteBuilder(
                          pageBuilder: (context, a1, a2) {
                            return LoginScreen();
                          },
                          transitionsBuilder: (context, a1, a2, child) {
                            return child; // I want only a Hero animation between the screens, nothing else
                          },
                          transitionDuration: const Duration(milliseconds: 1000),
                        ),
                      );
                    }
                  },
                ),
              ],
            ),
            Expanded(
              flex: 4,
              child: Hero(tag: 'logoWText', child: Image(image: AssetImage(kImageLogoWText))),
            ),
          ],
        ),
      ),
    );
  }
}

This ads a second of waiting for the app to load, with a spinner showing, but I find that most times, the spinner barely has time to show anyway, and in any case, it is a much better user experience to wait a sec for the app to load than to experience jank during use!

Other tips

If your screen transitions are still janky, even if you have run them back and forth, then you probably need to trim the performance of the screens involved. Perhaps it is your build method that's too big, and has too many things going on in it? Maybe certain widgets get rebuilt many times during each build of the screen?

Check out these pieces of advice and see if it helps: https://docs.flutter.dev/perf/best-practices In any case, they should improve your app's performance in general.

Try adding a small delay before loading the initial tasks in page B. Maybe with a Future.delayed()

Related