跳至主要内容

其他

您可以传递 --comments 来保留输出中的某些注释。默认情况下,它将保留以“!”开头的注释和包含“@preserve”、“@copyright”、“@license”或“@cc_on”(IE 的条件编译)的 JSDoc 样式注释。您可以传递 --comments all 来保留所有注释,或者传递一个有效的 JavaScript 正则表达式来仅保留与此正则表达式匹配的注释。例如,--comments /^!/ 将保留像 /*! Copyright Notice */ 这样的注释。

但是请注意,在某些情况下,注释可能会丢失。例如

function f() {
/** @preserve Foo Bar */
function g() {
// this function is never called
}
return something();
}

即使它有“@preserve”,注释也会丢失,因为内部函数 g(注释所附加到的 AST 节点)被压缩器丢弃,因为它没有被引用。

放置版权信息(或需要保留在输出中的其他信息)最安全的注释是附加到顶级节点的注释。

unsafe compress 选项

它启用了一些*可能*会在某些人为情况下破坏代码逻辑的转换,但对于大多数代码来说应该是可以的。它假设标准的内置 ECMAScript 函数和类没有被更改或替换。您可能想在自己的代码上尝试一下;它应该可以减小压缩后的大小。以下是启用此选项时进行的一些优化示例

  • new Array(1, 2, 3)Array(1, 2, 3)[ 1, 2, 3 ]
  • Array.from([1, 2, 3])[1, 2, 3]
  • new Object(){}
  • String(exp)exp.toString()"" + exp
  • new Object/RegExp/Function/Error/Array (...) → 我们丢弃 new
  • "foo bar".substr(4)"bar"

条件编译

您可以使用 --define (-d) 开关来声明全局变量,Terser 将假设这些变量是常量(除非在作用域中定义)。例如,如果您传递 --define DEBUG=false,那么结合死代码删除,Terser 将从输出中丢弃以下内容

if (DEBUG) {
console.log("debug stuff");
}

您可以使用 --define env.DEBUG=false 的形式指定嵌套常量。

另一种方法是在单独的文件中将全局变量声明为常量,并将其包含在构建中。例如,您可以有一个包含以下内容的 build/defines.js 文件

var DEBUG = false;
var PRODUCTION = true;
// etc.

并像这样构建您的代码

terser build/defines.js js/foo.js js/bar.js... -c

Terser 会注意到这些常量,并且由于它们不能被更改,它会将对它们的引用评估为值本身,并像往常一样删除不可达的代码。如果您使用 const 声明,则构建将包含它们。如果您针对的是不支持 const 的 < ES6 环境,则使用带有 reduce_varsvar(默认启用)就足够了。

条件编译 API

您还可以通过编程 API 使用条件编译。不同之处在于属性名称是 global_defs 并且是一个压缩器属性

var result = await minify(fs.readFileSync("input.js", "utf8"), {
compress: {
dead_code: true,
global_defs: {
DEBUG: false
}
}
});

要将标识符替换为任意的非常量表达式,需要在 global_defs 键前加上 "@",以指示 Terser 将该值解析为表达式

await minify("alert('hello');", {
compress: {
global_defs: {
"@alert": "console.log"
}
}
}).code;
// returns: 'console.log("hello");'

否则它将被替换为字符串字面量

await minify("alert('hello');", {
compress: {
global_defs: {
"alert": "console.log"
}
}
}).code;
// returns: '"console.log"("hello");'

注解

Terser 中的注解是一种告诉它以不同方式处理某个函数调用的方法。可以使用以下注解

  • /*@__INLINE__*/ - 强制在某处内联函数。
  • /*@__NOINLINE__*/ - 确保被调用的函数没有被内联到调用站点。
  • /*@__PURE__*/ - 将函数调用标记为纯函数。这意味着可以安全地删除它。
  • /*@__KEY__*/ - 将字符串字面量标记为属性,以便在压缩属性时也压缩它。
  • /*@__MANGLE_PROP__*/ - 在启用属性压缩器时,选择加入对象属性(或类字段)进行压缩。

您可以在开头使用 @ 符号或 # 符号。

以下是一些关于如何使用它们的示例

/*@__INLINE__*/
function_always_inlined_here()

/*#__NOINLINE__*/
function_cant_be_inlined_into_here()

const x = /*#__PURE__*/i_am_dropped_if_x_is_not_used()

function lookup(object, key) { return object[key]; }
lookup({ i_will_be_mangled_too: "bar" }, /*@__KEY__*/ "i_will_be_mangled_too");

ESTree / SpiderMonkey AST

Terser 有自己的抽象语法树格式;由于实际原因,我们不能轻易地更改为在内部使用 SpiderMonkey AST。但是,Terser 现在有一个可以导入 SpiderMonkey AST 的转换器。

例如,Acorn 是一个超快的解析器,可以生成 SpiderMonkey AST。它有一个小的 CLI 实用程序,可以解析一个文件并将 AST 以 JSON 格式转储到标准输出中。要使用 Terser 来压缩和混淆它

acorn file.js | terser -p spidermonkey -m -c

-p spidermonkey 选项告诉 Terser 所有输入文件都不是 JavaScript,而是以 JSON 格式描述的 SpiderMonkey AST 中的 JS 代码。因此,在这种情况下,我们不使用我们自己的解析器,而是将 AST 转换为我们的内部 AST。

spidermonkeyminify 中也可用作 parseformat 选项,以接受和/或生成 spidermonkey AST。

使用 Acorn 进行解析

更有趣的是,我添加了 -p acorn 选项,它将使用 Acorn 来完成所有解析。如果您传递此选项,Terser 将 require("acorn")

Acorn 真的很快(例如,在一些 650K 的代码上,250 毫秒而不是 380 毫秒),但是转换 Acorn 生成的 SpiderMonkey 树需要另外 150 毫秒,所以总的来说,它比仅仅使用 Terser 自己的解析器要多一点。

Terser 快速压缩模式

这不是很为人所知,但对于大多数 JavaScript 来说,空格删除和符号压缩占了压缩代码大小减少的 95% - 而不是复杂的代码转换。可以简单地禁用 compress 来将 Terser 构建速度提高 3 到 4 倍。

d3.js大小gzip 大小时间(秒)
原始451,131108,733-
terser@3.7.5 mangle=false, compress=false316,60085,2450.82
terser@3.7.5 mangle=true, compress=false220,21672,7301.45
terser@3.7.5 mangle=true, compress=true212,04670,9545.87
babili@0.1.4210,71372,14012.64
babel-minify@0.4.3210,32172,24248.67
babel-minify@0.5.0-alpha.01eac1c3210,42172,23814.17

要从 CLI 启用快速压缩模式,请使用

terser file.js -m

要使用 API 启用快速压缩模式,请使用

await minify(code, { compress: false, mangle: true });

源代码地图和调试

已知各种简化、重新排列、内联和删除代码的 compress 转换会对使用源代码地图进行调试产生不利影响。这是预料之中的,因为代码经过了优化,并且映射通常是不可能的,因为某些代码不再存在。为了在源代码地图调试中获得最高的保真度,请禁用 compress 选项,而只使用 mangle

调试时,请确保启用**“映射作用域”**功能,以将压缩后的变量名映射回其原始名称。
如果没有这个,所有变量的值都将是 undefined。有关更多详细信息,请参阅 https://github.com/terser/terser/issues/1367



image

编译器假设

为了更好地优化,编译器会做出各种假设

  • .toString().valueOf() 没有副作用,并且对于内置对象,它们没有被覆盖。
  • undefinedNaNInfinity 没有被外部重新定义。
  • 没有使用 arguments.calleearguments.callerFunction.prototype.caller
  • 代码不期望 Function.prototype.toString()Error.prototype.stack 的内容是特定的。
  • 在普通对象上获取和设置属性不会导致其他副作用(使用 .watch()Proxy)。
  • 可以添加、删除和修改对象属性(不会被 Object.defineProperty()Object.defineProperties()Object.freeze()Object.preventExtensions()Object.seal() 阻止)。
  • document.all 不等于 null
  • 为类分配属性没有副作用,也不会抛出异常。

使用 Terser 的构建工具和适配器

https://npmjs.net.cn/browse/depended/terser

在使用 yarn 的项目中将 uglify-es 替换为 terser

许多 JS 打包器和 uglify 包装器仍在使用错误版本的 uglify-es,并且尚未升级到 terser。如果您使用的是 yarn,则可以将以下别名添加到项目的 package.json 文件中

  "resolutions": {
"uglify-es": "npm:terser"
}

以便在所有深度嵌套的依赖项中使用 terser 而不是 uglify-es,而无需更改任何代码。

注意:要使此更改生效,您必须运行以下命令以删除现有的 yarn 锁定文件并重新安装所有软件包

$ rm -rf node_modules yarn.lock
$ yarn