Flutter: ツリーシェイキングの動作を確認

Flutter: ツリーシェイキングの動作を確認

July 14, 2024

参考

DevTools でのアプリケーションのサイズ確認について

ツリーシェイキングについて

ビルド後のアプリケーションのサイズの確認方法 #

Flutter のプロジェクトを作成し、以下コードのビルド後のサイズを確認するとします。

flutter create myapp
// main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

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

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

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

ビルドコマンドに --analyze-size のオプションをつけて実行します。これによってビルド後のアプリケーションのサイズに関する情報をまとめたファイルが生成されるようになります。

flutter build apk --analyze-size --target-platform=android-arm64

ビルドが完了すると次のメッセージが表示されます。

✓ Built build/app/outputs/flutter-apk/app-release.apk (6.4MB)
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
app-release.apk (total compressed)                                          6 MB
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  META-INF/
    CERT.SF                                                                 3 KB
    CERT.RSA                                                              1020 B
    MANIFEST.MF                                                             2 KB
  classes.dex                                                             273 KB
  lib/
    arm64-v8a                                                               6 MB
    Dart AOT symbols accounted decompressed size                            2 MB
      package:flutter                                                       1 MB
      dart:core                                                           243 KB
      dart:typed_data                                                     165 KB
      dart:ui                                                             104 KB
      dart:collection                                                      78 KB
      dart:async                                                           73 KB
      dart:convert                                                         48 KB
      dart:io                                                              27 KB
      dart:isolate                                                         24 KB
      package:vector_math/
        vector_math_64.dart                                                22 KB
      dart:ffi                                                             12 KB
      dart:math                                                             2 KB
      dart:mirrors                                                          1 KB
      dart:developer                                                       954 B
      package:myapp/
        main.dart                                                          531 B
      package:collection/
        src/
          priority_queue.dart                                              434 B
      dart:nativewrappers                                                  169 B
      dart:vmservice_io                                                    108 B
      void/
        <optimized out>                                                     60 B
      Never                                                                 33 B
  assets/
    flutter_assets                                                        196 KB
  AndroidManifest.xml                                                       1 KB
  res/
    CG.png                                                                  1 KB
  resources.arsc                                                           22 KB
  kotlin/
    collections                                                             1 KB
    kotlin.kotlin_builtins                                                  4 KB
    reflect                                                                 1 KB
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
A summary of your APK analysis can be found at: /Users/jane-doe/.flutter-devtools/apk-code-size-analysis_01.json

To analyze your app size in Dart DevTools, run the following command:
dart devtools --appSizeBase=/Users/jane-doe/.flutter-devtools/apk-code-size-analysis_01.json

アプリケーション全体のサイズである app-release.apk のサイズが 6 MB だとわかります。そして、その中でも package:myapp/main.dart531 B ということがわかります。

メッセージの最後で案内されているコマンドを実行すると GUI でより詳細に情報を確認することができます。以下を実行します。パスは各自の環境に応じて読み替えてください。

dart devtools --appSizeBase=/Users/jane-doe/.flutter-devtools/apk-code-size-analysis_01.json

すると、ブラウザで次のページが開きます。

Dart DevTools - App Size

左下の “Library of Class” セクションから、次のように選択します。

Root
  > lib
    > arm64-v8a
      > libapp.so (Dart AOT)
        > package:myapp/package:myapp/main.dart

これで表示されている内容が、自分が作成した myapp プロジェクトにおける各コードごとのサイズです。

例えば MyApp ウィジェットは 0.2 KBMyWidget ウィジェットは 0.1 KB のサイズであることがわかります。

Dart DevTools - App Size

ツリーシェイキングの確認(基本編) #

次のコードでビルドしサイズを確認します。(ビルド1)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

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

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

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        MyWidget1(),
        MyWidget2(),
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Text("Hello.");
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Text("Goodbye.");
  }
}

次に上記のコードの Column() から MyWidget2() をコメントアウトして、ビルドしサイズを確認します。(ビルド2)

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        MyWidget1(),
        // MyWidget2(),
      ],
    );
  }

結果は次の通りです。ビルド1は合計 0.8 KB となっていて、MyWidget1MyWidget2 が含まれている一方、ビルド2は 0.6 KB となっていて、MyWidget2 が含まれていないことが分かります。

ビルド1

Dart DevTools - App Size

ビルド2

Dart DevTools - App Size

ツリーシェイキングの確認(バレルファイル編) #

先ほどの例では1ファイルの中にウィジェットの記述がまとめられていましたが、たとえファイルが分割されてもツリーシェイキングが行われることに変わりはありません。以下のようなファイル構成で試してみます。

└ lib/
  ├ main.dart
  └ widget/
    ├ barrel.dart
    ├ my_widget_1.dart
    └ my_widget_2.dart
  • my_widget_1.dart は先ほどの MyWidget1 の内容と同じ。
  • my_widget_2.dart は先ほどの MyWidget2 の内容と同じ。
  • barrel.dart はバレルファイル。my_widget_1.dartmy_widget_2.dart をまとめてエスクポートしているだけ。
  • main.dartbarrel.dart から import した MyWidget1MyWidget2Column() で表示。
// barrel.dart

export './my_widget_1.dart';
export './my_widget_2.dart';
// main.dart

import 'package:flutter/material.dart';

import 'widget/barrel.dart';

void main() {
  runApp(const MyApp());
}

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

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

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

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        MyWidget1(),
        MyWidget2(),
      ],
    );
  }
}

この結果をビルド1とします。次に上記コードから Column()MyWidget2() をコメントアウトします。この結果をビルド2とします。

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        MyWidget1(),
        // MyWidget2(),
      ],
    );
  }

両者の結果は次のとおりです。

ビルド1

Dart DevTools - App Size

ビルド2

Dart DevTools - App Size

一つ前の項で確認したのと同様に、ファイルが分割されていたとしてもアプリケーションで使われていないコードはツリーシェイキングの対象となり、ビルド後のサイズには影響を与えないことが分かります。

ツリーシェイキングの確認(デバッグ時のみ実行されるコード編) #

if (kDebugMode) {} での分岐や、assert(() { return true; }()) といった書き方をすると、そのコードは Flutter のデバッグ実行時のみ有効なコードとなります。つまり、リリースビルド時はツリーシェイキングの対象となります。

次のコードをビルドしてみます。

// logger.dart

import 'package:flutter/foundation.dart';

/// Prints a message in all mode including release mode.
void printForRelease() {
  print("Hello World!");
}

/// Prints a message only in debug mode.
/// "kDebugMode" is true in debug mode and false otherwise,
/// so this function is only executed in debug mode.
void printForDebug1() {
  if (kDebugMode) {
    print("Hello World!");
  }
}

/// Prints a message only in debug mode.
/// "assert" is enabled in debug mode and disabled in release mode,
/// so this function is only executed in debug mode.
void printForDebug2() {
  assert(() {
    print("Hello World!");
    return true;
  }());
}
// main.dart

import 'package:flutter/material.dart';

import 'logger.dart';

void main() {
  runApp(const MyApp());
}

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

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

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

  @override
  Widget build(BuildContext context) {
    return TextButton(
      child: const Text("Tap to print"),
      onPressed: () {
        printForRelease();
        printForDebug1();
        printForDebug2();
      },
    );
  }
}

左下の “Library of Class” セクションから、logger.dart のファイルを見てみます。すると、printForRelease() の関数しか含まれていないことが確認できます。デバッグ時にしか有効でない関数はツリーシェイキングされているということですね。

Dart DevTools - App Size

なお、上記の例では print() 系のコードをすべて関数にしていましたが、これを1つのクラスにまとめた場合でも適切にツリーシェイキングされます。

// logger.dart

import 'package:flutter/foundation.dart';

class Logger {
  /// Prints a message in all mode including release mode.
  static void printForRelease() {
    print("Hello World!");
  }

  /// Prints a message only in debug mode.
  /// "kDebugMode" is true in debug mode and false otherwise,
  /// so this function is only executed in debug mode.
  static void printForDebug1() {
    if (kDebugMode) {
      print("Hello World!");
    }
  }

  /// Prints a message only in debug mode.
  /// "assert" is enabled in debug mode and disabled in release mode,
  /// so this function is only executed in debug mode.
  static void printForDebug2() {
    assert(() {
      print("Hello World!");
      return true;
    }());
  }
}

Dart DevTools - App Size