From 85486ffffc164cc8f7eeeca31603a83b620a088d Mon Sep 17 00:00:00 2001 From: Nate Biggs Date: Sat, 28 Jun 2025 13:46:44 -0400 Subject: [PATCH] Allow minification to be toggled for wasm builds (#171211) Provides extra minification controls for web builds. Updates the existing `--minify` flag to pass its value to both JS and wasm (instead of just JS). Also adds two new flags `--minify-js` and `--minify-wasm` which toggle minification for just one of the web targets. While JS minification has a huge impact on code size, wasm minification has a much smaller effect on the program's size. Since minification can obfuscate useful info (like errors) one may want to turn off minification for wasm but leave it on for JS. The plan is to temporarily use `--no-minify-wasm` for devtools to help debug some exceptions being seen. --------- Co-authored-by: Nate Biggs --- .../lib/src/commands/build_web.dart | 19 ++- .../lib/src/web/compiler_config.dart | 5 + .../hermetic/build_web_test.dart | 119 ++++++++++++++- .../build_system/targets/web_test.dart | 136 +++++++++--------- 4 files changed, 209 insertions(+), 70 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/build_web.dart b/packages/flutter_tools/lib/src/commands/build_web.dart index 003503dfa0e..d6788bff5f2 100644 --- a/packages/flutter_tools/lib/src/commands/build_web.dart +++ b/packages/flutter_tools/lib/src/commands/build_web.dart @@ -99,9 +99,16 @@ class BuildWebCommand extends BuildSubCommand { hide: !verboseHelp, ); argParser.addFlag( - 'minify', + 'minify-js', help: - 'Generate minified output. ' + 'Generate minified output for js. ' + 'If not explicitly set, uses the compilation mode (debug, profile, release).', + hide: !verboseHelp, + ); + argParser.addFlag( + 'minify-wasm', + help: + 'Generate minified output for wasm. ' 'If not explicitly set, uses the compilation mode (debug, profile, release).', hide: !verboseHelp, ); @@ -175,7 +182,8 @@ class BuildWebCommand extends BuildSubCommand { ); final bool sourceMaps = boolArg('source-maps'); - final bool? minify = argResults!.wasParsed('minify') ? boolArg('minify') : null; + final bool? minifyJs = argResults!.wasParsed('minify-js') ? boolArg('minify-js') : null; + final bool? minifyWasm = argResults!.wasParsed('minify-wasm') ? boolArg('minify-wasm') : null; final List compilerConfigs; @@ -194,11 +202,12 @@ class BuildWebCommand extends BuildSubCommand { optimizationLevel: optimizationLevel, stripWasm: boolArg('strip-wasm'), sourceMaps: sourceMaps, + minify: minifyWasm, ), JsCompilerConfig( csp: boolArg('csp'), dumpInfo: boolArg('dump-info'), - minify: minify, + minify: minifyJs, nativeNullAssertions: boolArg('native-null-assertions'), noFrequencyBasedMinification: boolArg('no-frequency-based-minification'), optimizationLevel: jsOptimizationLevel, @@ -210,7 +219,7 @@ class BuildWebCommand extends BuildSubCommand { JsCompilerConfig( csp: boolArg('csp'), dumpInfo: boolArg('dump-info'), - minify: minify, + minify: minifyJs, nativeNullAssertions: boolArg('native-null-assertions'), noFrequencyBasedMinification: boolArg('no-frequency-based-minification'), optimizationLevel: jsOptimizationLevel, diff --git a/packages/flutter_tools/lib/src/web/compiler_config.dart b/packages/flutter_tools/lib/src/web/compiler_config.dart index aeb19c7de30..f2be41e88dd 100644 --- a/packages/flutter_tools/lib/src/web/compiler_config.dart +++ b/packages/flutter_tools/lib/src/web/compiler_config.dart @@ -146,6 +146,7 @@ class WasmCompilerConfig extends WebCompilerConfig { const WasmCompilerConfig({ super.optimizationLevel, this.stripWasm = true, + this.minify = true, super.sourceMaps = true, super.renderer = WebRendererMode.defaultForWasm, }); @@ -156,6 +157,8 @@ class WasmCompilerConfig extends WebCompilerConfig { /// Whether to strip the wasm file of static symbols. final bool stripWasm; + final bool? minify; + @override CompileTarget get compileTarget => CompileTarget.wasm; @@ -179,6 +182,7 @@ class WasmCompilerConfig extends WebCompilerConfig { '-O${optimizationLevelForBuildMode(buildMode)}', '--${stripSymbols ? '' : 'no-'}strip-wasm', if (!sourceMaps) '--no-source-maps', + if (minify ?? buildMode == BuildMode.release) '--minify' else '--no-minify', if (buildMode == BuildMode.debug) '--extra-compiler-option=--enable-asserts', ]; } @@ -188,6 +192,7 @@ class WasmCompilerConfig extends WebCompilerConfig { final Map settings = { ...super._buildKeyMap, kStripWasm: stripWasm, + 'minify': minify, WebCompilerConfig.kSourceMapsEnabled: sourceMaps, }; return jsonEncode(settings); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart index e67b699fbf1..66d048c98d3 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart @@ -435,7 +435,7 @@ void main() { ); testUsingContext( - 'Defaults to web renderer skwasm mode for wasm when no option is specified', + 'Defaults to web renderer skwasm mode and minify for wasm when no option is specified', () async { final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem); final CommandRunner runner = createTestCommandRunner(buildCommand); @@ -459,6 +459,123 @@ void main() { expect(configs[0].compileTarget, CompileTarget.wasm); expect(configs[1].renderer, WebRendererMode.canvaskit); expect(configs[1].compileTarget, CompileTarget.js); + + expect(configs[0].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[0].toCommandOptions(BuildMode.debug), contains('--no-minify')); + expect(configs[1].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[1].toCommandOptions(BuildMode.debug), contains('--no-minify')); + }), + }, + ); + + testUsingContext( + 'Passes minify to only wasm when minify-wasm specified', + () async { + final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem); + final CommandRunner runner = createTestCommandRunner(buildCommand); + setupFileSystemForEndToEndTest(fileSystem); + await runner.run(['build', 'web', '--no-pub', '--wasm', '--minify-wasm']); + }, + overrides: { + Platform: () => fakePlatform, + FileSystem: () => fileSystem, + FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), + ProcessManager: () => processManager, + BuildSystem: + () => TestBuildSystem.all(BuildResult(success: true), ( + Target target, + Environment environment, + ) { + final List configs = (target as WebServiceWorker).compileConfigs; + + expect(configs[0].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[0].toCommandOptions(BuildMode.debug), contains('--minify')); + expect(configs[1].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[1].toCommandOptions(BuildMode.debug), contains('--no-minify')); + }), + }, + ); + + testUsingContext( + 'Passes no-minify to wasm when no-minify-wasm specified', + () async { + final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem); + final CommandRunner runner = createTestCommandRunner(buildCommand); + setupFileSystemForEndToEndTest(fileSystem); + await runner.run(['build', 'web', '--no-pub', '--wasm', '--no-minify-wasm']); + }, + overrides: { + Platform: () => fakePlatform, + FileSystem: () => fileSystem, + FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), + ProcessManager: () => processManager, + BuildSystem: + () => TestBuildSystem.all(BuildResult(success: true), ( + Target target, + Environment environment, + ) { + final List configs = (target as WebServiceWorker).compileConfigs; + + expect(configs[0].toCommandOptions(BuildMode.release), contains('--no-minify')); + expect(configs[0].toCommandOptions(BuildMode.debug), contains('--no-minify')); + expect(configs[1].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[1].toCommandOptions(BuildMode.debug), contains('--no-minify')); + }), + }, + ); + + testUsingContext( + 'Passes minify to js when minify-js specified', + () async { + final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem); + final CommandRunner runner = createTestCommandRunner(buildCommand); + setupFileSystemForEndToEndTest(fileSystem); + await runner.run(['build', 'web', '--no-pub', '--wasm', '--minify-js']); + }, + overrides: { + Platform: () => fakePlatform, + FileSystem: () => fileSystem, + FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), + ProcessManager: () => processManager, + BuildSystem: + () => TestBuildSystem.all(BuildResult(success: true), ( + Target target, + Environment environment, + ) { + final List configs = (target as WebServiceWorker).compileConfigs; + + expect(configs[0].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[0].toCommandOptions(BuildMode.debug), contains('--no-minify')); + expect(configs[1].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[1].toCommandOptions(BuildMode.debug), contains('--minify')); + }), + }, + ); + + testUsingContext( + 'Passes no-minify to js when no-minify-js specified', + () async { + final TestWebBuildCommand buildCommand = TestWebBuildCommand(fileSystem: fileSystem); + final CommandRunner runner = createTestCommandRunner(buildCommand); + setupFileSystemForEndToEndTest(fileSystem); + await runner.run(['build', 'web', '--no-pub', '--wasm', '--no-minify-js']); + }, + overrides: { + Platform: () => fakePlatform, + FileSystem: () => fileSystem, + FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), + ProcessManager: () => processManager, + BuildSystem: + () => TestBuildSystem.all(BuildResult(success: true), ( + Target target, + Environment environment, + ) { + final List configs = (target as WebServiceWorker).compileConfigs; + + expect(configs[0].toCommandOptions(BuildMode.release), contains('--minify')); + expect(configs[0].toCommandOptions(BuildMode.debug), contains('--no-minify')); + expect(configs[1].toCommandOptions(BuildMode.release), contains('--no-minify')); + expect(configs[1].toCommandOptions(BuildMode.debug), contains('--no-minify')); }), }, ); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart index 8033697e59d..13842a7bd93 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart @@ -1138,75 +1138,82 @@ name: foo ]) { for (final String buildMode in const ['profile', 'release', 'debug']) { for (final bool sourceMaps in const [true, false]) { - test( - 'Dart2WasmTarget invokes dart2wasm with renderer=$renderer, -O$level, stripping=$strip, defines=$defines, modeMode=$buildMode sourceMaps=$sourceMaps', - () => testbed.run(() async { - final int expectedLevel = - level ?? - switch (buildMode) { - 'debug' => 0, - 'profile' || 'release' => 2, - _ => throw UnimplementedError(), - }; - environment.defines[kBuildMode] = buildMode; - environment.defines[kDartDefines] = encodeDartDefines(defines); + for (final bool minify in const [true, false]) { + test( + 'Dart2WasmTarget invokes dart2wasm with renderer=$renderer, -O$level, stripping=$strip, defines=$defines, modeMode=$buildMode sourceMaps=$sourceMaps minify=$minify', + () => testbed.run(() async { + final int expectedLevel = + level ?? + switch (buildMode) { + 'debug' => 0, + 'profile' || 'release' => 2, + _ => throw UnimplementedError(), + }; + environment.defines[kBuildMode] = buildMode; + environment.defines[kDartDefines] = encodeDartDefines(defines); - final File depFile = environment.buildDir.childFile('dart2wasm.d'); + final File depFile = environment.buildDir.childFile('dart2wasm.d'); - final File outputJsFile = environment.buildDir.childFile('main.dart.mjs'); - processManager.addCommand( - FakeCommand( - command: [ - ..._kDart2WasmLinuxArgs, - '-Ddart.vm.profile=${buildMode == 'profile'}', - '-Ddart.vm.product=${buildMode == 'release'}', - if (buildMode != 'debug') ...[ - '--extra-compiler-option=--delete-tostring-package-uri=dart:ui', - '--extra-compiler-option=--delete-tostring-package-uri=package:flutter', + final File outputJsFile = environment.buildDir.childFile('main.dart.mjs'); + processManager.addCommand( + FakeCommand( + command: [ + ..._kDart2WasmLinuxArgs, + '-Ddart.vm.profile=${buildMode == 'profile'}', + '-Ddart.vm.product=${buildMode == 'release'}', + if (buildMode != 'debug') ...[ + '--extra-compiler-option=--delete-tostring-package-uri=dart:ui', + '--extra-compiler-option=--delete-tostring-package-uri=package:flutter', + ], + if (renderer == WebRendererMode.skwasm) ...[ + '--extra-compiler-option=--import-shared-memory', + '--extra-compiler-option=--shared-memory-max-pages=32768', + ], + ...defines.map((String define) => '-D$define'), + if (renderer == WebRendererMode.skwasm) ...[ + '-DFLUTTER_WEB_USE_SKIA=false', + '-DFLUTTER_WEB_USE_SKWASM=true', + ], + if (renderer == WebRendererMode.canvaskit) ...[ + '-DFLUTTER_WEB_USE_SKIA=true', + '-DFLUTTER_WEB_USE_SKWASM=false', + ], + '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', + '--extra-compiler-option=--depfile=${depFile.absolute.path}', + '-O$expectedLevel', + if (strip && buildMode == 'release') + '--strip-wasm' + else + '--no-strip-wasm', + if (!sourceMaps) '--no-source-maps', + if (minify) '--minify' else '--no-minify', + if (buildMode == 'debug') '--extra-compiler-option=--enable-asserts', + '-o', + environment.buildDir.childFile('main.dart.wasm').absolute.path, + environment.buildDir.childFile('main.dart').absolute.path, ], - if (renderer == WebRendererMode.skwasm) ...[ - '--extra-compiler-option=--import-shared-memory', - '--extra-compiler-option=--shared-memory-max-pages=32768', - ], - ...defines.map((String define) => '-D$define'), - if (renderer == WebRendererMode.skwasm) ...[ - '-DFLUTTER_WEB_USE_SKIA=false', - '-DFLUTTER_WEB_USE_SKWASM=true', - ], - if (renderer == WebRendererMode.canvaskit) ...[ - '-DFLUTTER_WEB_USE_SKIA=true', - '-DFLUTTER_WEB_USE_SKWASM=false', - ], - '-DFLUTTER_WEB_CANVASKIT_URL=https://www.gstatic.com/flutter-canvaskit/abcdefghijklmnopqrstuvwxyz/', - '--extra-compiler-option=--depfile=${depFile.absolute.path}', - '-O$expectedLevel', - if (strip && buildMode == 'release') '--strip-wasm' else '--no-strip-wasm', - if (!sourceMaps) '--no-source-maps', - if (buildMode == 'debug') '--extra-compiler-option=--enable-asserts', - '-o', - environment.buildDir.childFile('main.dart.wasm').absolute.path, - environment.buildDir.childFile('main.dart').absolute.path, - ], - onRun: - (_) => - outputJsFile - ..createSync() - ..writeAsStringSync('foo'), - ), - ); + onRun: + (_) => + outputJsFile + ..createSync() + ..writeAsStringSync('foo'), + ), + ); - await Dart2WasmTarget( - WasmCompilerConfig( - optimizationLevel: level, - stripWasm: strip, - renderer: renderer, - sourceMaps: sourceMaps, - ), - ).build(environment); + await Dart2WasmTarget( + WasmCompilerConfig( + optimizationLevel: level, + stripWasm: strip, + renderer: renderer, + sourceMaps: sourceMaps, + minify: minify, + ), + ).build(environment); - expect(outputJsFile.existsSync(), isTrue); - }, overrides: {ProcessManager: () => processManager}), - ); + expect(outputJsFile.existsSync(), isTrue); + }, overrides: {ProcessManager: () => processManager}), + ); + } } } } @@ -1257,6 +1264,7 @@ name: foo WasmCompilerConfig(optimizationLevel: 0), WasmCompilerConfig(renderer: WebRendererMode.canvaskit), WasmCompilerConfig(stripWasm: false), + WasmCompilerConfig(minify: false), // All properties non-default WasmCompilerConfig(