Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 1eaf5c0

Browse files
[flutter_tools] tree shake icons from web builds (#115886)
* wip * remove temp text file * fix tests * add test * default to off * restore gitignore * update * apply annotation to cupertino icons as well * update reference to library in icon_tree_shaker.dart * update tests * fix tests * remove hack to skip non-const check on web * add hint about how much reduction and test
1 parent ada4460 commit 1eaf5c0

File tree

8 files changed

+83
-42
lines changed

8 files changed

+83
-42
lines changed

packages/flutter/lib/src/cupertino/icons.dart

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import 'package:flutter/widgets.dart';
5959
/// See also:
6060
///
6161
/// * [Icon], used to show these icons.
62+
@staticIconProvider
6263
class CupertinoIcons {
6364
// This class is not meant to be instantiated or extended; this constructor
6465
// prevents instantiation and extension.

packages/flutter/lib/src/material/icons.dart

+1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ class PlatformAdaptiveIcons implements Icons {
149149
/// * [IconButton]
150150
/// * <https://material.io/resources/icons>
151151
/// * [AnimatedIcons], for the list of available animated Material Icons.
152+
@staticIconProvider
152153
class Icons {
153154
// This class is not meant to be instantiated or extended; this constructor
154155
// prevents instantiation and extension.

packages/flutter/lib/src/widgets/icon_data.dart

+11
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,14 @@ class IconDataProperty extends DiagnosticsProperty<IconData> {
9393
return json;
9494
}
9595
}
96+
97+
class _StaticIconProvider {
98+
const _StaticIconProvider();
99+
}
100+
101+
/// Annotation for classes that only provide static const [IconData] instances.
102+
///
103+
/// This is a hint to the font tree shaker to ignore the constant instances
104+
/// of [IconData] appearing in the class when tracking which code points
105+
/// should be retained in the bundled font.
106+
const Object staticIconProvider = _StaticIconProvider();

packages/flutter_tools/lib/src/build_system/targets/common.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ class KernelSnapshot extends Target {
190190
// Force linking of the platform for desktop embedder targets since these
191191
// do not correctly load the core snapshots in debug mode.
192192
// See https://github.com/flutter/flutter/issues/44724
193-
bool forceLinkPlatform;
193+
final bool forceLinkPlatform;
194194
switch (targetPlatform) {
195195
case TargetPlatform.darwin:
196196
case TargetPlatform.windows_x64:

packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart

+18-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:meta/meta.dart';
56
import 'package:mime/mime.dart' as mime;
67
import 'package:process/process.dart';
78

@@ -150,8 +151,6 @@ class IconTreeShaker {
150151
/// Calls font-subset, which transforms the [input] font file to a
151152
/// subsetted version at [outputPath].
152153
///
153-
/// All parameters are required.
154-
///
155154
/// If [enabled] is false, or the relative path is not recognized as an icon
156155
/// font used in the Flutter application, this returns false.
157156
/// If the font-subset subprocess fails, it will [throwToolExit].
@@ -161,6 +160,7 @@ class IconTreeShaker {
161160
required String outputPath,
162161
required String relativePath,
163162
}) async {
163+
164164
if (!enabled) {
165165
return false;
166166
}
@@ -212,9 +212,23 @@ class IconTreeShaker {
212212
_logger.printError(await utf8.decodeStream(fontSubsetProcess.stderr));
213213
throw IconTreeShakerException._('Font subsetting failed with exit code $code.');
214214
}
215+
_logger.printStatus(getSubsetSummaryMessage(input, _fs.file(outputPath)));
215216
return true;
216217
}
217218

219+
@visibleForTesting
220+
String getSubsetSummaryMessage(File inputFont, File outputFont) {
221+
final String fontName = inputFont.basename;
222+
final double inputSize = inputFont.lengthSync().toDouble();
223+
final double outputSize = outputFont.lengthSync().toDouble();
224+
final double reductionBytes = inputSize - outputSize;
225+
final String reductionPercentage = (reductionBytes / inputSize * 100).toStringAsFixed(1);
226+
return 'Font asset "$fontName" was tree-shaken, reducing it from '
227+
'${inputSize.ceil()} to ${outputSize.ceil()} bytes '
228+
'($reductionPercentage% reduction). Tree-shaking can be disabled '
229+
'by providing the --no-tree-shake-icons flag when building your app.';
230+
}
231+
218232
/// Returns a map of { fontFamily: relativePath } pairs.
219233
Future<Map<String, String>> _parseFontJson(
220234
String fontManifestData,
@@ -268,6 +282,8 @@ class IconTreeShaker {
268282
'--kernel-file', appDill.path,
269283
'--class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
270284
'--class-name', 'IconData',
285+
'--annotation-class-name', '_StaticIconProvider',
286+
'--annotation-class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
271287
];
272288
_logger.printTrace('Running command: ${cmd.join(' ')}');
273289
final ProcessResult constFinderProcessResult = await _processManager.run(cmd);

packages/flutter_tools/lib/src/commands/build_web.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class BuildWebCommand extends BuildSubCommand {
1919
required FileSystem fileSystem,
2020
required bool verboseHelp,
2121
}) : _fileSystem = fileSystem, super(verboseHelp: verboseHelp) {
22-
addTreeShakeIconsFlag(enabledByDefault: false);
22+
addTreeShakeIconsFlag();
2323
usesTargetOption();
2424
usesOutputDir();
2525
usesPubOption();

packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ void main() {
137137
'DartDefines': 'Zm9vPWE=,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==',
138138
'DartObfuscation': 'false',
139139
'TrackWidgetCreation': 'false',
140-
'TreeShakeIcons': 'false',
140+
'TreeShakeIcons': 'true',
141141
});
142142
}),
143143
});
@@ -187,7 +187,7 @@ void main() {
187187
'DartDefines': 'RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==',
188188
'DartObfuscation': 'false',
189189
'TrackWidgetCreation': 'false',
190-
'TreeShakeIcons': 'false',
190+
'TreeShakeIcons': 'true',
191191
});
192192
}),
193193
});

packages/flutter_tools/test/general.shard/build_system/targets/icon_tree_shaker_test.dart

+48-36
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ void main() {
4040
'--kernel-file', appDillPath,
4141
'--class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
4242
'--class-name', 'IconData',
43+
'--annotation-class-name', '_StaticIconProvider',
44+
'--annotation-class-library-uri', 'package:flutter/src/widgets/icon_data.dart',
4345
];
4446

4547
void addConstFinderInvocation(
@@ -227,13 +229,18 @@ void main() {
227229
fileSystem: fileSystem,
228230
artifacts: artifacts,
229231
);
230-
231232
final CompleterIOSink stdinSink = CompleterIOSink();
232233
addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
233234
resetFontSubsetInvocation(stdinSink: stdinSink);
234-
235+
// Font starts out 2500 bytes long
236+
final File inputFont = fileSystem.file(inputPath)
237+
..writeAsBytesSync(List<int>.filled(2500, 0));
238+
// after subsetting, font is 1200 bytes long
239+
fileSystem.file(outputPath)
240+
..createSync(recursive: true)
241+
..writeAsBytesSync(List<int>.filled(1200, 0));
235242
bool subsetted = await iconTreeShaker.subsetFont(
236-
input: fileSystem.file(inputPath),
243+
input: inputFont,
237244
outputPath: outputPath,
238245
relativePath: relativePath,
239246
);
@@ -249,6 +256,10 @@ void main() {
249256
expect(subsetted, true);
250257
expect(stdinSink.getAndClear(), '59470\n');
251258
expect(processManager, hasNoRemainingExpectations);
259+
expect(
260+
logger.statusText,
261+
contains('Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 2500 to 1200 bytes (52.0% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.'),
262+
);
252263
});
253264

254265
testWithoutContext('Does not subset a non-supported font', () async {
@@ -315,40 +326,41 @@ void main() {
315326
expect(subsetted, false);
316327
});
317328

318-
testWithoutContext('Non-constant instances', () async {
319-
final Environment environment = createEnvironment(<String, String>{
320-
kIconTreeShakerFlag: 'true',
321-
kBuildMode: 'release',
329+
for (final TargetPlatform platform in <TargetPlatform>[TargetPlatform.android_arm, TargetPlatform.web_javascript]) {
330+
testWithoutContext('Non-constant instances $platform', () async {
331+
final Environment environment = createEnvironment(<String, String>{
332+
kIconTreeShakerFlag: 'true',
333+
kBuildMode: 'release',
334+
});
335+
final File appDill = environment.buildDir.childFile('app.dill')
336+
..createSync(recursive: true);
337+
338+
final IconTreeShaker iconTreeShaker = IconTreeShaker(
339+
environment,
340+
fontManifestContent,
341+
logger: logger,
342+
processManager: processManager,
343+
fileSystem: fileSystem,
344+
artifacts: artifacts,
345+
);
346+
347+
addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid);
348+
349+
await expectLater(
350+
() => iconTreeShaker.subsetFont(
351+
input: fileSystem.file(inputPath),
352+
outputPath: outputPath,
353+
relativePath: relativePath,
354+
),
355+
throwsToolExit(
356+
message:
357+
'Avoid non-constant invocations of IconData or try to build'
358+
' again with --no-tree-shake-icons.',
359+
),
360+
);
361+
expect(processManager, hasNoRemainingExpectations);
322362
});
323-
final File appDill = environment.buildDir.childFile('app.dill')
324-
..createSync(recursive: true);
325-
326-
final IconTreeShaker iconTreeShaker = IconTreeShaker(
327-
environment,
328-
fontManifestContent,
329-
logger: logger,
330-
processManager: processManager,
331-
fileSystem: fileSystem,
332-
artifacts: artifacts,
333-
);
334-
335-
addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid);
336-
337-
await expectLater(
338-
() => iconTreeShaker.subsetFont(
339-
input: fileSystem.file(inputPath),
340-
outputPath: outputPath,
341-
relativePath: relativePath,
342-
),
343-
throwsToolExit(
344-
message:
345-
'Avoid non-constant invocations of IconData or try to build'
346-
' again with --no-tree-shake-icons.',
347-
),
348-
);
349-
expect(processManager, hasNoRemainingExpectations);
350-
});
351-
363+
}
352364
testWithoutContext('Non-zero font-subset exit code', () async {
353365
final Environment environment = createEnvironment(<String, String>{
354366
kIconTreeShakerFlag: 'true',

0 commit comments

Comments
 (0)