Skip to content

Commit

Permalink
perf(@angular-devkit/build-angular): use esbuild/terser combination t…
Browse files Browse the repository at this point in the history
…o optimize global scripts

`esbuild` and `terser` are now used to optimize global scripts in addition to the previous performance enhancement of optimizing application bundles. This change removes the need for the `terser-webpack-plugin` as a direct dependency and provides further production build time improvements for applications which use global scripts (`scripts` option in the `angular.json` file).
Since `esbuild` does not support optimizing a script with ES2015+ syntax when targetting ES5, the JavaScript optimizer will fallback to only using terser in the event that such a situation occurs. This will only happen if ES5 is the output target for an application and a global script contains ES2015+ syntax. In that case, the global script is technically already invalid for the target environment and may fail at runtime but this is and has been considered a configuration issue. Global scripts must be compatible with the target environment/browsers.
  • Loading branch information
clydin authored and alan-agius4 committed Aug 11, 2021
1 parent 1341ad6 commit 8e82263
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 47 deletions.
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -216,7 +216,6 @@
"tar": "^6.1.6",
"temp": "^0.9.0",
"terser": "5.7.1",
"terser-webpack-plugin": "5.1.4",
"text-table": "0.2.0",
"tree-kill": "1.2.2",
"ts-node": "^10.0.0",
Expand Down
1 change: 0 additions & 1 deletion packages/angular_devkit/build_angular/BUILD.bazel
Expand Up @@ -174,7 +174,6 @@ ts_library(
"@npm//stylus",
"@npm//stylus-loader",
"@npm//terser",
"@npm//terser-webpack-plugin",
"@npm//text-table",
"@npm//tree-kill",
"@npm//tslib",
Expand Down
1 change: 0 additions & 1 deletion packages/angular_devkit/build_angular/package.json
Expand Up @@ -63,7 +63,6 @@
"stylus": "0.54.8",
"stylus-loader": "6.1.0",
"terser": "5.7.1",
"terser-webpack-plugin": "5.1.4",
"text-table": "0.2.0",
"tree-kill": "1.2.2",
"tslib": "2.3.0",
Expand Down
Expand Up @@ -160,5 +160,43 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
harness.expectFile('dist/main.js').content.not.toMatch(/\sawait\s/);
harness.expectFile('dist/main.js').content.toContain('"for await...of"');
});

it('allows optimizing global scripts with ES2015+ syntax when targetting ES5', async () => {
await harness.modifyFile('src/tsconfig.app.json', (content) => {
const tsconfig = JSON.parse(content);
if (!tsconfig.compilerOptions) {
tsconfig.compilerOptions = {};
}
tsconfig.compilerOptions.target = 'es5';

return JSON.stringify(tsconfig);
});

// Add global script with ES2015+ syntax to the project
await harness.writeFile(
'src/es2015-syntax.js',
`
class foo {
bar() {
console.log('baz');
}
}
(new foo()).bar();
`,
);

harness.useTarget('build', {
...BASE_OPTIONS,
optimization: {
scripts: true,
},
scripts: ['src/es2015-syntax.js'],
});

const { result } = await harness.executeOnce();

expect(result?.success).toBe(true);
});
});
});
Expand Up @@ -31,9 +31,7 @@ import { WebpackConfigOptions } from '../../utils/build-options';
import { findCachePath } from '../../utils/cache-path';
import {
allowMangle,
allowMinify,
cachingDisabled,
maxWorkers,
persistentBuildCacheEnabled,
profilingEnabled,
} from '../../utils/environment-options';
Expand Down Expand Up @@ -298,34 +296,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
}

const extraMinimizers = [];

if (scriptsOptimization) {
const globalScriptsNames = globalScriptsByBundleName.map((s) => s.bundleName);

if (globalScriptsNames.length > 0) {
// Script bundles are fully optimized here in one step since they are never downleveled.
// They are shared between ES2015 & ES5 outputs so must support ES5.
// The `terser-webpack-plugin` will add the minified flag to the asset which will prevent
// additional optimizations by the next plugin.
const TerserPlugin = require('terser-webpack-plugin');
extraMinimizers.push(
new TerserPlugin({
parallel: maxWorkers,
extractComments: false,
include: globalScriptsNames,
terserOptions: {
ecma: 5,
compress: allowMinify,
output: {
ascii_only: true,
wrap_func_args: false,
},
mangle: allowMangle && platform !== 'server',
},
}),
);
}

extraMinimizers.push(
new JavaScriptOptimizerPlugin({
define: buildOptions.aot ? GLOBAL_DEFS_FOR_TERSER_WITH_AOT : GLOBAL_DEFS_FOR_TERSER,
Expand Down
Expand Up @@ -7,7 +7,7 @@
*/

import remapping from '@ampproject/remapping';
import { transform } from 'esbuild';
import { TransformFailure, transform } from 'esbuild';
import { minify } from 'terser';

/**
Expand Down Expand Up @@ -38,19 +38,41 @@ interface OptimizeRequest {

export default async function ({ asset, options }: OptimizeRequest) {
// esbuild is used as a first pass
const esbuildResult = await transform(asset.code, {
minifyIdentifiers: !options.keepNames,
minifySyntax: true,
// NOTE: Disabling whitespace ensures unused pure annotations are kept
minifyWhitespace: false,
pure: ['forwardRef'],
legalComments: options.removeLicenses ? 'none' : 'inline',
sourcefile: asset.name,
sourcemap: options.sourcemap && 'external',
define: options.define,
keepNames: options.keepNames,
target: `es${options.target}`,
});
let esbuildResult;
try {
esbuildResult = await transform(asset.code, {
minifyIdentifiers: !options.keepNames,
minifySyntax: true,
// NOTE: Disabling whitespace ensures unused pure annotations are kept
minifyWhitespace: false,
pure: ['forwardRef'],
legalComments: options.removeLicenses ? 'none' : 'inline',
sourcefile: asset.name,
sourcemap: options.sourcemap && 'external',
define: options.define,
keepNames: options.keepNames,
target: `es${options.target}`,
});
} catch (error) {
const failure = error as TransformFailure;

// If esbuild fails with only ES5 support errors, fallback to just terser.
// This will only happen if ES5 is the output target and a global script contains ES2015+ syntax.
// In that case, the global script is technically already invalid for the target environment but
// this is and has been considered a configuration issue. Global scripts must be compatible with
// the target environment.
if (
failure.errors?.every((error) =>
error.text.includes('to the configured target environment ("es5") is not supported yet'),
)
) {
esbuildResult = {
code: asset.code,
};
} else {
throw error;
}
}

// terser is used as a second pass
const terserResult = await optimizeWithTerser(
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Expand Up @@ -10369,7 +10369,7 @@ temp@^0.9.0:
mkdirp "^0.5.1"
rimraf "~2.6.2"

terser-webpack-plugin@5.1.4, terser-webpack-plugin@^5.1.3:
terser-webpack-plugin@^5.1.3:
version "5.1.4"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1"
integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==
Expand Down

0 comments on commit 8e82263

Please sign in to comment.