Flutter: ツリーシェイキングの動作を確認
July 14, 2024
参考
DevTools でのアプリケーションのサイズ確認について
- Measuring your app’s size | Flutter
- Use the app size tool | Flutter
ツリーシェイキングについて
- Excluding Dart code from the release compiled executable | Flutter Community | Medium
ビルド後のアプリケーションのサイズの確認方法 #
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.dart
は 531 B
ということがわかります。
メッセージの最後で案内されているコマンドを実行すると GUI でより詳細に情報を確認することができます。以下を実行します。パスは各自の環境に応じて読み替えてください。
dart devtools --appSizeBase=/Users/jane-doe/.flutter-devtools/apk-code-size-analysis_01.json
すると、ブラウザで次のページが開きます。
左下の “Library of Class” セクションから、次のように選択します。
Root
> lib
> arm64-v8a
> libapp.so (Dart AOT)
> package:myapp/package:myapp/main.dart
これで表示されている内容が、自分が作成した myapp
プロジェクトにおける各コードごとのサイズです。
例えば MyApp
ウィジェットは 0.2 KB
、MyWidget
ウィジェットは 0.1 KB
のサイズであることがわかります。
ツリーシェイキングの確認(基本編) #
次のコードでビルドしサイズを確認します。(ビルド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
となっていて、MyWidget1
と MyWidget2
が含まれている一方、ビルド2は 0.6 KB
となっていて、MyWidget2
が含まれていないことが分かります。
ビルド1
ビルド2
ツリーシェイキングの確認(バレルファイル編) #
先ほどの例では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.dart
とmy_widget_2.dart
をまとめてエスクポートしているだけ。main.dart
はbarrel.dart
からimport
したMyWidget1
とMyWidget2
をColumn()
で表示。
// 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
ビルド2
一つ前の項で確認したのと同様に、ファイルが分割されていたとしてもアプリケーションで使われていないコードはツリーシェイキングの対象となり、ビルド後のサイズには影響を与えないことが分かります。
ツリーシェイキングの確認(デバッグ時のみ実行されるコード編) #
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()
の関数しか含まれていないことが確認できます。デバッグ時にしか有効でない関数はツリーシェイキングされているということですね。
なお、上記の例では 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;
}());
}
}