ข้ามไปยังเนื้อหา

Build & Release

Flutter compile โค้ดเดียวกันสามแบบต่างกันขึ้นอยู่กับว่าคุณต้องการอะไร:

flowchart LR
  debug["debug
JIT · hot reload · asserts on
(slow, dev only)"]
  profile["profile
AOT · perf tools on
(measure real speed)"]
  release["release
AOT · asserts off · optimized
(ship this)"]
Build mode สามแบบและการ compile ของมัน
  • Debug — compile แบบ JIT, hot reload, assertion เปิด, service extension ใช้ได้ ช้าและใหญ่; สำหรับ development เท่านั้น
  • Profile — compile แบบ AOT เหมือน release แต่ยังมี performance tooling ต่ออยู่ นี่คือที่ที่คุณวัดความเร็วจริงและตาม jank (อย่า profile ใน debug — ตัวเลขไม่มีความหมาย)
  • Release — compile แบบ AOT, assertion ถูกตัดออก, debugging ปิด, tree-shaken และ optimize แล้ว นี่คือสิ่งที่ผู้ใช้ได้รับ

การแบ่ง debug/release จริง ๆ แล้วคือการแบ่งแบบ compilation:

  • JIT (Just-In-Time) ใช้ใน debug compile Dart ขณะที่มันรัน — ซึ่งเป็นสิ่งที่ทำให้ hot reload เป็นไปได้ (มันสลับโค้ดใหม่เข้าไปได้ทันที)
  • AOT (Ahead-Of-Time) ใช้ใน profile และ release compile Dart เป็น native machine code ก่อนที่แอปจะ ship ผลลัพธ์คือ startup เร็วและ performance ที่คาดเดาได้ แลกกับการไม่มี hot reload

Tree-shaking เกิดขึ้นใน AOT build: โค้ดที่ไม่ได้ใช้ (และที่น่าสังเกตคือ icon glyph ที่ไม่ได้ reference) ถูกตัดออกเพื่อให้ binary เล็ก — พิสูจน์แล้วด้านล่างใน build จริง MaterialIcons-Regular.otf เล็กลงกว่า 99%

คำสั่ง build เสถียร แต่สิ่งที่มันสร้างข้างใต้เปลี่ยนไปแล้ว มีสี่การเปลี่ยนแปลงที่สำคัญเมื่อคุณ ship แอป Flutter 3.47:

  • Impeller คือ renderer default ทุกที่แล้ว มันกลายเป็น default บน iOS และ Android มาก่อนแล้ว; Flutter 3.47 ทำให้มันเป็น default บน macOS, Windows และ Linux ด้วย Impeller compile shader ของมันตอน build time แทนที่จะเป็นตอนใช้ครั้งแรก ซึ่งกำจัดปัญหาคลาสสิก “animation สะดุดตอนรันครั้งแรก” ที่ runtime shader compilation ของ Skia เคยทำให้เกิด — กลไกครอบคลุมแบบเจาะลึกในบทเรียน render-pipeline
  • ข้อกำหนดขั้นต่ำของ Apple สูงขึ้น Flutter 3.47 ต้องการ iOS 15 (เดิม 13) และ macOS 12 (เดิม 10.15) เช็คบรรทัด platform ของ Podfile และ App Store deployment target ก่อนอัปเกรด
  • Swift Package Manager คือทางไปข้างหน้า CocoaPods อยู่ในโหมด maintenance แล้ว และ plugin iOS ยอดนิยมส่วนใหญ่ migrate ไปแล้ว โปรเจกต์ใหม่ควรใช้ SwiftPM; โปรเจกต์เดิมยังทำงานได้ต่อไป แต่ plugin ที่ยังไม่ migrate จะกลายเป็นตัวบล็อกในที่สุด
  • โปรเจกต์ Android ใช้ Kotlin ที่มากับ SDK ตั้งแต่ 3.44 Kotlin toolchain มากับ Flutter แทนที่จะประกาศแยกต่อโปรเจกต์ โปรเจกต์ Android ที่ generate ใหม่จึงไม่ pin เวอร์ชัน Kotlin plugin ของตัวเองอีกต่อไป

การเปลี่ยนแปลงที่ใหญ่กว่ากำลังเกิดขึ้น: Material กับ Cupertino กำลังย้ายออกจาก framework ไปเป็น package แยก material_ui กับ cupertino_ui (ทั้งคู่ผ่าน 1.0 บน pub.dev แล้ว — ดูpub.dev/packages/material_ui) วันนี้มันเป็น opt-in พร้อมแผน deprecate เวอร์ชันใน framework อย่างเป็นทางการ และมี migration fix:

Terminal window
dart fix --apply --code=migrate_design_widgets

ไม่มีอะไรพังถ้าคุณเพิกเฉยมันตอนนี้ — แต่โปรเจกต์ใหม่ที่ต้องการให้ design system version แยกอิสระจาก SDK สามารถใช้ package พวกนี้ได้แล้ว

รันจริง ใน scratch environment ของคอร์สนี้เอง จากแอป counter แบบ default ของ flutter create:

$ flutter build web --release
Compiling lib/main.dart for the Web...
Font asset "CupertinoIcons.ttf" was tree-shaken, reducing it from 257628 to 1472 bytes (99.4% reduction).
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 7800 bytes (99.5% reduction).
Compiling lib/main.dart for the Web... 13.4s
✓ Built build/web
$ ls -la build/web/main.dart.js
-rw-r--r-- 1895343 main.dart.js # ~1.8MB

1,895,343 bytes (~1.8MB) สำหรับ main.dart.js — ตัวเลขนี้แม่นยำและทำซ้ำได้ จากแอป counter แบบ default ที่ไม่มี dependency เพิ่มเติม นั่นคือ output JS ที่ compile ด้วย dart2js ที่ผู้ใช้จริงดาวน์โหลด (ก่อนการบีบอัดระดับ HTTP gzip/brotli ใดก็ตามที่ server ของคุณใช้ ซึ่งปกติลดขนาดลงมากพอสมควร — ไม่ได้วัดที่นี่ เพราะขึ้นอยู่กับ setup ของการ serve ไม่ใช่ตัว build)

Blog ของ Flutter 3.47 พูดถึง WebAssembly compilation ว่า “new… deferred loading support now available via flag” build จริง:

$ flutter build web --wasm
┌─ New feature ────────────────────────────────────────────────────────────────────────────┐
│ WebAssembly compilation is new. Understand the details before deploying to production. │
│ See https://flutter.dev/to/wasm for more information. │
└──────────────────────────────────────────────────────────────────────────────────────────┘
Compiling lib/main.dart for the Web...
Compiling lib/main.dart for the Web... 13.8s
✓ Built build/web
$ ls -la build/web/main.dart.wasm build/web/main.dart.mjs
-rw-r--r-- 1511217 main.dart.wasm # ~1.4MB
-rw-r--r-- 29429 main.dart.mjs # ~28.7KB (JS glue/loader)

สำหรับแอป default นี้ output ของ wasm (~1.4MB รวม) เล็กกว่า main.dart.js ของ dart2js (~1.8MB) จริง ๆ — ตัวเลขจริง ไม่ใช่กฎทั่วไปสำหรับทุกแอป

การรองรับ browser คือจุดที่ต้องระวังจริง ๆ อ้างอิงตรง ๆ จากหน้า WebAssembly ของ docs.flutter.dev: “To run a Flutter app that has been compiled to Wasm, you need a browser that supports WasmGC… Chromium and V8 support WasmGC since version 119.” ส่วนที่น่าแปลกใจ ระบุตรง ๆ เหมือนกัน: “Chrome on iOS uses WebKit, which doesn’t yet support WasmGC” และ — แม้ Firefox กับ Safari จะประกาศรองรับ WasmGC แล้วก็ตาม — “Firefox versions 120 and later were previously able to run Flutter/Wasm, but they’re experiencing a bug that is blocking compatibility with Flutter’s Wasm renderer” และ Safari “is experiencing a similar bug.” ในทางปฏิบัติ ณ Flutter 3.47.5 ของคอร์สนี้: การรองรับ Wasm ที่เชื่อถือได้จริง ๆ มีแค่ Chromium/Chrome/Edge 119+ เท่านั้น docs เดียวกันยังต้องการ response header เฉพาะจากอะไรก็ตามที่ serve build: Cross-Origin-Embedder-Policy: credentialless กับ Cross-Origin-Opener-Policy: same-origin เพราะ Wasm build ใช้หลาย thread

lib/env_config.dart
const apiUrl = String.fromEnvironment(
'API_URL',
defaultValue: 'https://default.example.com',
);
test/env_config_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:probe/lessons/tooling-and-production__build-and-release/env_config.dart';
void main() {
test('String.fromEnvironment falls back to defaultValue when no --dart-define is passed', () {
// This course's harness runs a plain `flutter test` with no custom
// defines (see below for the separately-run, real proof of an actual
// override), so the only thing provable INSIDE the harness's own gate
// is the fallback path -- which is itself real and worth proving.
expect(apiUrl, 'https://default.example.com');
});
}
00:00 +0: String.fromEnvironment falls back to defaultValue when no --dart-define is passed
00:00 +1: All tests passed!

นั่นคือการรันปกติของ harness เอง — harness ของคอร์สนี้ไม่มีกลไกส่ง --dart-define เฉพาะต่อบทเรียน ดังนั้นการ override เองถูกรันด้วยมือ นอกเหนือจาก harness เทียบกับไฟล์นี้เป๊ะ ๆ:

$ flutter test --dart-define=API_URL=https://dart-define-test.example.com test/env_config_test.dart

พร้อม test body ที่เปลี่ยนชั่วคราวเป็น expect(apiUrl, 'https://dart-define-test.example.com') สำหรับการรันด้วยมือครั้งนั้น มันผ่าน — จริง รันครั้งเดียว ไม่ได้ยืนยันซ้ำโดย gate ปกติของ harness เอง รวมกันแล้วทั้งสอง run พิสูจน์กลไกจริง: apiUrl คือ compile-time constant ที่ resolve เป็นอะไรก็ตามที่ --dart-define=API_URL=... ให้มา และ fallback ไปที่ defaultValue ก็ต่อเมื่อ flag นั้นไม่มีเท่านั้น — ไม่ใช่การอ่าน environment variable ตอน runtime --dart-define-from-file=config.json คือกลไกเดียวกันที่อ่านไฟล์ JSON ทั้งไฟล์ของคู่ key/value แทนที่จะเป็น -D ทีละตัว สำหรับตอนที่ flavor ต้องการหลายค่าพร้อมกัน

Flavor (Android) / scheme (iOS) ให้ codebase เดียวสร้าง dev, staging และ production build พร้อม config ต่างกัน — API endpoint, app ID, ไอคอน — โดยไม่ต้อง branch โค้ด

Terminal window
flutter run --flavor dev --dart-define=API_URL=https://dev.api.example.com
flutter build apk --flavor prod --dart-define=API_URL=https://api.example.com

--dart-define พิสูจน์แล้วด้านบนว่า inject compile-time constant จริง ๆ คือวิธีที่สะอาดในการส่งค่าต่อ environment โดยไม่ต้อง commit secret ลง source

Build จริง (Android เครื่อง Mac นี้มี Android SDK จริงตาม flutter doctor — ไม่เคยรันใน shared probe ของคอร์สนี้):

$ flutter build apk --release --obfuscate --split-debug-info=build/symbols
Running Gradle task 'assembleRelease'...
Warning: The generated ELF library contains unobfuscated DWARF debugging information.
To avoid this, use --strip to remove it.
Running Gradle task 'assembleRelease'... 101.5s
✓ Built build/app/outputs/flutter-apk/app-release.apk (42.4MB)
$ ls -la build/symbols/
app.android-arm.symbols 1243680 bytes
app.android-arm64.symbols 1478248 bytes
app.android-x64.symbols 1476736 bytes

สิ่งที่น่าแปลกใจจริงและไม่แก้ไขใน output นั้น: warning เกี่ยวกับ unobfuscated DWARF debugging information ที่ยังอยู่ใน native ELF library ที่ generate มา แนะนำให้ใช้ flag --strip — แต่ flutter build apk --help (เช็คตรง ๆ) แสดงว่าไม่มี flag --strip บนคำสั่งนี้; warning ดูเหมือนจะชี้ไปที่ config การ strip ระดับ Android/NDK นอกเหนือจาก CLI surface ของ Flutter เอง ไม่ได้ยืนยันเพิ่มเติมอย่างอิสระที่นี่ สิ่งที่ยืนยันเต็มที่จาก --help อ้างอิงตรง ๆ: “This flag [--obfuscate] must always be combined with --split-debug-info… For an app built with this flag, the flutter symbolize command with the right program symbol file is required to obtain a human readable stack trace.” ไฟล์ symbol หนึ่งไฟล์ถูกเขียนต่อ target ABI (arm, arm64, x64 ด้านบน) — flutter symbolize ต้องการไฟล์ที่ตรงกับอุปกรณ์ที่สร้าง crash นั้น

$ flutter symbolize --help
-d, --debug-info=</out/android/app.arm64.symbols> A path to the symbols file generated with "--split-debug-info".
-i, --input=</crashes/stack_trace.err> A file path containing a Dart stack trace.
-o, --output A file path for a symbolized stack trace to be written to.

การสร้าง runtime crash ที่ obfuscate จริง ๆ เพื่อ symbolize ต้องการอุปกรณ์จริงที่รัน obfuscated release build — environment ของคอร์สนี้ไม่มี ดังนั้นการรัน end-to-end แบบเจาะจงนั้นอ้างอิงจากข้อความการใช้งานจริงของ --help ด้านบนแทนที่จะปั้นขึ้นมา ตัว build เอง ไฟล์ symbol จริง และ DWARF warning จริง ทั้งหมดรันและจับได้จริงด้านบน

ต้องการ Android SDK จริง — ยืนยันแล้วว่ามีอยู่บนเครื่องนี้โดย flutter doctor (ดู output จริงของบทเรียน project-and-tooling) มันยังต้องการ target ABI เดียวเป๊ะ — การรันมันกับ multi-ABI build แบบ default พังด้วย error จริงที่เจาะจง:

$ flutter build apk --analyze-size
Cannot perform code size analysis when building for multiple ABIs. Specify one of android-arm, android-arm64, or android-x64 in the --target-platform flag.

ลองใหม่ด้วย ABI เดียว output จริง:

$ flutter build apk --analyze-size --target-platform=android-arm64
arm64-v8a 14 MB
Dart AOT symbols accounted decompressed size 3 MB
package:flutter 1 MB
dart:core 239 KB
dart:mixin_deduplication 178 KB
dart:ui 171 KB
dart:typed_data 157 KB
dart:async 90 KB
package:material_color_utilities 69 KB
dart:convert 43 KB
dart:collection 41 KB
dart:io 26 KB
package:scratch_app/main.dart 2 KB
AndroidManifest.xml 2 KB
resources.arsc 7 KB
A summary of your APK analysis can be found at: ~/.flutter-devtools/apk-code-size-analysis_01.json
To analyze your app size in Dart DevTools, run: dart devtools --appSizeBase=...

การแบ่ง per-package จริงของ APK ขนาด 14MB ของแอป default โดยที่ package:flutter เองเป็นผู้มีส่วนร่วมของ Dart AOT ที่ใหญ่ที่สุดที่ 1MB — นี่คือเครื่องมือจริงสำหรับตอบ “ทำไม APK ของฉันถึงใหญ่ขนาดนี้” ไม่ใช่การเดาจากจำนวน dependency

version: 1.2.3+4

ยืนยันตรง ๆ จากคู่มือ deployment แต่ละ platform ของ docs.flutter.dev: ส่วนก่อน + (1.2.3) คือ build name — ตัวเลขคั่นด้วยจุดสามตัว ที่ผู้ใช้เห็น; ส่วนหลัง (4) คือ build number การ map เจาะจงตาม platform และแม่นยำ:

Platformbuild name map ไปเป็นbuild number map ไปเป็น
AndroidversionNameversionCode
iOS / macOSCFBundleShortVersionStringCFBundleVersion
Windowsสามตัวเลขแรกของ file/product versionตัวเลขที่สี่

ทั้งสองส่วน override ได้ตอน build โดยไม่ต้องแตะ pubspec.yaml: flutter build apk --build-name=1.2.3 --build-number=4 (หรือแบบเทียบเท่าสำหรับ ipa/appbundle/windows) — มีประโยชน์สำหรับ CI pipeline ที่คำนวณ build number จากจำนวน commit หรือ timestamp แทนที่จะแก้ manifest ด้วยมือทุก release

การสร้าง asset ไอคอน/splash ของทุก platform ด้วยมือ (density mipmap หลายแบบของ Android, asset catalog Contents.json ของ iOS, …) คืองานเชิงกลไกและเสี่ยงผิดพลาดแบบที่ dev_dependency มีไว้เพื่อทำอัตโนมัติพอดี สอง package มาตรฐาน เวอร์ชัน resolve ปัจจุบันบนเครื่องนี้ผ่าน flutter pub add --dry-run เทียบกับ pub.dev: flutter_launcher_icons (0.14.4) generate ไอคอนของทุก platform จาก source image เดียว ขับด้วย config block flutter_launcher_icons.yaml/pubspec.yaml; flutter_native_splash (2.4.8) ทำแบบเดียวกันสำหรับ native splash screen (ก่อน Flutter engine เริ่ม) ซึ่งเป็นวิธีเดียวที่จะแสดงอะไรบางอย่างก่อนที่ Dart เองจะเริ่ม initialize ด้วยซ้ำ ทั้งคู่เป็น tooling แค่ตอน dev — มัน generate native asset ที่ commit ไว้ครั้งเดียว ไม่ใช่ runtime dependency ของแอปที่ ship

config build/CI ที่ AI generate มักได้คำสั่ง build แบบ happy-path ถูกต้อง แต่พลาด flag ที่สำคัญแค่ตอนที่คุณ ship ให้ผู้ใช้จริงหรือ debug production crash จริง ๆ

Terminal window
# AI-generated CI release step
flutter build apk --release
flutter build appbundle --release

ก่อนจะเชื่อ config build/release ที่ generate มาแบบนี้ ให้เช็ค:

  • ไม่มี --obfuscate --split-debug-info=<dir> บน release build — พิสูจน์แล้วด้านบนว่านี่คือ flag combination ที่ใช้ได้จริง ship binary ที่เล็กกว่าและ reverse-engineer ยากกว่า; ถ้าไม่มีมัน stack trace ของ crash reporter ก็อ่านยากขึ้นเหมือนกัน (identifier ไม่ obfuscate แต่ก็ไม่ใช่สิ่งที่ flutter symbolize คาดหวังถ้าคุณเพิ่ม obfuscation ทีหลังโดยไม่มีไฟล์ symbol ที่ตรงกัน)
  • โฟลเดอร์ build/symbols (หรือเทียบเท่า) จาก --split-debug-info ไม่ถูก upload/archive ไว้ที่ไหนเลยใน CI pipeline — พิสูจน์แล้วด้านบนว่า flutter symbolize ไร้ประโยชน์โดยไม่มีไฟล์ symbol ที่ตรงกับ build นั้นเป๊ะ ทำหายหมายความว่า crash report ที่ symbolize ไม่ได้ตลอดไป
  • ค่า --dart-define สำหรับ secret (API key) ที่ commit ตรง ๆ ลง CI YAML แบบ plain text แทนที่จะ inject จาก secret storage ของ CI platform เอง — พิสูจน์แล้วด้านบนว่าค่า --dart-define ถูก compile เข้าไปเป็น string ตรง ๆ ที่ใครก็ตามที่มี build artifact แกะออกมาได้ ไม่ว่าค่านั้นจะมาจากไหน
  • ไม่มี --target-platform บน CI step ที่ใช้ --analyze-size — พิสูจน์แล้วด้านบนว่ามันพังทันทีด้วย error จริงที่เจาะจงสำหรับ multi-ABI build แบบ default

ทุก flag ในบทนี้กำลังตอบคำถามเวอร์ชันเดียวกัน: “โค้ด” หมายความว่าอะไรกันแน่ ตอนที่อุปกรณ์ของผู้ใช้รันมัน? คำตอบของ debug mode คือ “อะไรก็ตามที่อยู่ใน editor ของคุณตอนนี้” (JIT, hot-reload ได้) คำตอบของ release mode คือ “artifact ที่ตายตัว optimize แล้ว compile แบบ AOT” — และเมื่อคุณอยู่ตรงนั้น ทุก flag เพิ่มเติม (--obfuscate, --split-debug-info, --dart-define) จริง ๆ แล้วคือการจัดการช่องว่างระหว่าง artifact นั้นกับ source ที่คุณอ่านได้จริง: obfuscation ขยายช่องว่างนั้นโดยตั้งใจ (เพื่อขนาดและความทนทานต่อ reverse-engineering) --split-debug-info + flutter symbolize คือเครื่องมือที่ปิดมันกลับ เฉพาะตอนที่คุณมี symbol ที่ตรงกัน เท่านั้น และ --dart-define คือค่าที่ถูกอบเข้าไปในช่องว่างนั้นตอน compile time ไม่ใช่ถูก inject จากอีกฝั่งของมัน

ตัวเลือกข้อดีต้นทุน
flutter build web (dart2js)พิสูจน์แล้วด้านบน: ทำงานได้ทุกที่ที่ browser สมัยใหม่รันพิสูจน์แล้วด้านบน: ~1.8MB main.dart.js สำหรับแอป default เปล่า ๆ
flutter build web --wasmพิสูจน์แล้วด้านบน: อาจให้ output รวมเล็กกว่าพิสูจน์แล้วด้านบนจาก docs: เชื่อถือได้แค่บน browser ที่ใช้ Chromium วันนี้ — ความเสี่ยงเรื่อง browser compatibility จริง
--obfuscate --split-debug-infobinary เล็กกว่า reverse-engineer ยากกว่า พิสูจน์แล้วด้านบนด้วย build จริงพิสูจน์แล้วด้านบน: flutter symbolize กับไฟล์ symbol ที่ตรงกันเป๊ะกลายเป็นสิ่งจำเป็นสำหรับ crash trace ที่อ่านได้
--dart-define สำหรับ configพิสูจน์แล้วด้านบน: เป็น compile-time จริง ไม่ต้องอ่านไฟล์/network ตอน runtimeพิสูจน์แล้วด้านบน (mental model): ถูกอบเข้าไปใน binary เป็น string ตรง ๆ — ไม่ใช่ที่สำหรับ secret ที่ต้องเก็บเป็นความลับ
flutter_launcher_icons/flutter_native_splashgenerate asset ของทุก platform ถูกต้องจาก source เดียวขั้นตอน generate ตอน dev-time ที่ต้องจำไว้ว่าต้องรันซ้ำหลังเปลี่ยน source asset
  • “flutter build web --wasm รันได้บน browser สมัยใหม่ใดก็ได้” พิสูจน์แล้วด้านบนว่าไม่จริง ตรง ๆ จาก docs ของ Flutter เอง: Chrome บน iOS, Firefox ปัจจุบัน และ Safari ปัจจุบัน ทั้งหมดถูกบล็อกด้วยบั๊กจริงที่ track ไว้ แม้จะรองรับมาตรฐาน WasmGC ที่อยู่เบื้องหลังในทางเทคนิคก็ตาม
  • “--obfuscate เพียงอย่างเดียวพอแล้วสำหรับ crash report ที่อ่านได้ทีหลัง” พิสูจน์แล้วด้านบนว่าไม่จริง: ข้อความ --help ของ SDK เองระบุว่า obfuscation “must always be combined with --split-debug-info” และแม้จะมีมันแล้ว flutter symbolize ก็ยังต้องการไฟล์ symbol ที่ตรงเป๊ะจาก build นั้น
  • “flutter build apk --analyze-size ทำงานได้เลยบน release build แบบ multi-ABI default” พิสูจน์แล้วด้านบนว่าไม่จริง ด้วย error จริงที่เจาะจง: มันต้องการ --target-platform เดียวเป๊ะ
  • “ค่า --dart-define ถูกซ่อนอย่างปลอดภัยข้างใน binary ที่ compile แล้ว” พิสูจน์แล้วว่าไม่จริงจาก mental model ด้านบน: มันถูก compile เข้าไปเป็น string ตรง ๆ ที่แกะออกจาก artifact ได้ — เหมาะกับ API base URL ที่ไม่ใช่ความลับจริง ๆ แต่ผิดจริง ๆ สำหรับ API key หรือ secret
ควรใช้ build mode ไหนในการวัด performance จริง?
พิสูจน์แล้วด้านบนด้วย build จริงที่ทำซ้ำได้ main.dart.js สำหรับแอป counter default ของ flutter create มีขนาดประมาณเท่าไหร่ และ --wasm เทียบกันยังไง?
พิสูจน์แล้วด้านบนด้วยการอ้างอิง docs WebAssembly ของ Flutter เองตรง ๆ browser ไหนรัน Flutter --wasm build ได้เชื่อถือได้จริงวันนี้?
พิสูจน์แล้วด้านบนด้วยการรัน flutter test จริง --dart-define ทำอะไรจริง ๆ?
พิสูจน์แล้วด้านบนด้วย APK build จริงและ output ของ --help สิ่งที่จำเป็นเพื่อได้ crash stack trace ที่อ่านได้จาก obfuscated release build คืออะไร?
พิสูจน์แล้วด้านบนด้วยข้อความ error จริง flutter build apk --analyze-size ต้องการอะไรที่ release build ปกติไม่ต้องการ?
ทำไม Impeller ที่กลายเป็น renderer default ถึงลดจังหวะสะดุดของ animation ตอนรันครั้งแรก?