Search code examples
androidflutterlag

What is SkSL and "Shader Jank Compilation" all about in Flutter?


The Problem:

So, I built a login screen and the startup time of that page sucks! After I looked into the documentation then I found something called Shader Jank Complation, I found the documentation confusing but decided to implement what's given there.

After doing the steps I ran the app and the performance increased drastically. But, I didn't get what's happening in the background and what was the documentation actually pointing to.

I also didn't understand what was wrong with the code and that the app didn't performed as per expectations or lagged for no reason.

The Code:

Login Screen:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:regexed_validator/regexed_validator.dart';

import '../constants/buttons.dart';
import '../constants/colors.dart';
import '../constants/text_style.dart';

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

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {

  String email = "";
  String password = "";

  final formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: SizedBox(
          width: double.infinity,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text(
                "Login",
                textAlign: TextAlign.center,
                style: MyTextStyle.headingTextWelcomeScreen,
              ),
              const SizedBox(
                height: 10,
              ),
              Text(
                "Enter your email and password.",
                textAlign: TextAlign.center,
                style: MyTextStyle.defaultText,
              ),
              const SizedBox(
                height: 30,
              ),
              Form(
                  key: formKey,
                  child: Column(
                    children: [
                      SizedBox(
                        width: MediaQuery.of(context).size.width * 0.7,
                        child: TextFormField(
                          inputFormatters: [
                            FilteringTextInputFormatter.deny(' ')
                          ],
                          keyboardType: TextInputType.emailAddress,
                          cursorColor: MyColors.blue,
                          style: MyTextStyle.defaultBlackText,
                          decoration: InputDecoration(
                            prefixIcon: Icon(
                              Icons.email_outlined,
                              color: MyColors.blue,
                            ),
                            label: Text(
                              "Email",
                              style: MyTextStyle.defaultBlueText,
                            ),
                            enabledBorder: OutlineInputBorder(
                              borderSide: BorderSide(
                                color: MyColors.blue,
                                width: 2,
                              ),
                              borderRadius: const BorderRadius.all(
                                Radius.circular(10),
                              ),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderSide: BorderSide(
                                color: MyColors.blue,
                                width: 2,
                              ),
                              borderRadius: const BorderRadius.all(
                                Radius.circular(10),
                              ),
                            ),
                          ),
                          onChanged: (value) {
                            email = value;
                          },
                          validator: (value) {
                            if (validator.email(value!)) {
                              return null;
                            } else {
                              return 'Invalid Email';
                            }
                          },
                        ),
                      ),
                      const SizedBox(
                        height: 10,
                      ),
                      SizedBox(
                        width: MediaQuery.of(context).size.width * 0.7,
                        child: TextFormField(
                          inputFormatters: [
                            FilteringTextInputFormatter.deny(' ')
                          ],
                          obscureText: true,
                          keyboardType: TextInputType.visiblePassword,
                          cursorColor: MyColors.blue,
                          style: MyTextStyle.defaultBlackText,
                          decoration: InputDecoration(
                              prefixIcon: Icon(
                                Icons.password_outlined,
                                color: MyColors.blue,
                              ),
                              label: Text("Password",
                                  style: MyTextStyle.defaultBlueText),
                              enabledBorder: OutlineInputBorder(
                                borderSide: BorderSide(
                                  color: MyColors.blue,
                                  width: 2,
                                ),
                                borderRadius: const BorderRadius.all(
                                  Radius.circular(10),
                                ),
                              ),
                              focusedBorder: OutlineInputBorder(
                                borderSide: BorderSide(
                                  color: MyColors.blue,
                                  width: 2,
                                ),
                                borderRadius: const BorderRadius.all(
                                  Radius.circular(10),
                                ),
                              ),
                              errorStyle: MyTextStyle.defaultRedText
                                  .copyWith(fontSize: 12),
                              errorMaxLines: 2),
                          onChanged: (value) {
                            password = value;
                          },
                          validator: (value) {
                            if (validator.password(value!)) {
                              return null;
                            } else {
                              if (password.length < 8) {
                                return 'Invalid Password: Must be grater than 7 characters';
                              } else {
                                return 'Invalid Password: Missing one of the characters @, Captial Letter, Number.';
                              }
                            }
                          },
                        ),
                      ),
                    ],
                  )),
              const SizedBox(
                height: 15,
              ),
              SizedBox(
                height: 45,
                width: MediaQuery.of(context).size.width * 0.7,
                child: DefaultButton(
                  buttonText: "Login",
                  vdc: () {
                    formKey.currentState!.validate();
                  },
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Welcome Screen:

import 'package:attendance_management_system_firebase/pages/reg_page.dart';
import 'package:flutter/material.dart';

import '../constants/buttons.dart';
import '../constants/text_style.dart';
import 'login_page.dart';

class WelcomePage extends StatefulWidget {
  static const String id = "welcome_sc";
  const WelcomePage({super.key});

  @override
  State<WelcomePage> createState() => _WelcomePageState();
}

class _WelcomePageState extends State<WelcomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
          child: Center(
        child: Padding(
          padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
          child: SingleChildScrollView(
            child: Column(
              children: [
                Image.asset(
                  "assets/welcome.png",
                  width: MediaQuery.of(context).size.width * 0.65,
                ),
                const SizedBox(
                  height: 35,
                ),
                Text(
                  "Welcome",
                  textAlign: TextAlign.center,
                  style: MyTextStyle.headingTextWelcomeScreen,
                ),
                Text(
                  "Let's get you started with taking attendance!",
                  textAlign: TextAlign.center,
                  style: MyTextStyle.defaultText,
                ),
                const SizedBox(
                  height: 20,
                ),
                SizedBox(
                  height: 45,
                  width: MediaQuery.of(context).size.width * 0.7,
                  child: DefaultButton(
                    buttonText: "Login",
                    vdc: () {
                      Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (context) => const LoginPage()));
                    },
                  ),
                ),
                const SizedBox(
                  height: 5,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      "Don't have an account? ",
                      style: MyTextStyle.defaultBlueText,
                    ),
                    InkWell(
                      onTap: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => const RegisterPage(),
                          ),
                        );
                      },
                      child: Text(
                        "Register",
                        style: MyTextStyle.defaultBlueText.copyWith(
                            fontWeight: FontWeight.w600,
                            decoration: TextDecoration.underline),
                      ),
                    ),
                  ],
                )
              ],
            ),
          ),
        ),
      )),
    );
  }
}

main.dart

import 'package:attendance_management_system_firebase/pages/welcome_page.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: WelcomePage(),
    );
  }
}

Solution

  • There is no problem with your app. Most of the apps, at the start, have shader compilation jank. Flutter under the Skia rendering engine, and this GPU engine when the app opens for the first time generates shaders.

    Shaders are the low-level code given to GPU to draw UI on screen. When an app is launched for the first time, there are no shaders present in the app. So, when the app is launched, to present a smooth UI,** a frame should render within 16ms. Shaders help to perform a series of commands repeatedly**. Think of it as a Component Widget which we use in high-level language for cleaning up our duplicate code. Shaders and Widgets work alike but shaders are more low-level code. So, back to the topic, when we open the app for the first time, Skia sometimes compiles and generates shaders when running app for the first time, to present the next frames with the same draw commands as fast as possible without needing to generate the frames again and again.

    The generation of shaders requires quite a bit of time(approx. 200ms or more depending on the situation) and this causes the frame, which can't be rendered within 16ms to jank. This jank is called shader-compilation-jank.

    Flutter devs are working on a new graphics engine called Impeller. This will solve this problem, but it is still in development. So to solve this issue temporarily, we precompile these shaders and bundle them with the app.

    So, the step you did was when you ran your app in profile mode, skia generated those shaders and then saved it in a file. And these shaders are pre-bundled with the app so that skia doesn't have to compile and generate these shaders again.

    Hope it's clear.