分享一款超好用的JavaScript 打包压缩工具


Posted in Javascript onApril 26, 2020

背景

平时大家在开发 Js 项目的时候,可能已经离不开 webpack 等打包工具了。而 webpack 打包速度大概就是“能用“的水平。大概去年开始,我就开始在构想,如果能写一个极速的打包工具,功能未必需要很强,可能对小项目非常有用。去年我用 C++ 写完 parser 之后,便没什么动力写下去了。但是最近发现有这个想法的不止我一个,Figma 的 CTO 业余之际写了一个打包器 https://github.com/evanw/esbuild ,可以说完完全全实现了我想象中的需求,不过他是用 Go 语言实现的。我看到这个项目时心里一想,这不是我去年就想做的事吗,这 push 我赶紧把打包压缩部分完成。

代码

Github 地址: https://github.com/vincentdchan/jetpack.js

优化思路

并行 Parsing

毫无疑问,每一个 js 文件的 parsing 可以在不同线程完成,这就需要支持并行的语言。由于 parsing 的结果是 AST,所以需要可以共享内存的语言(排除通过 messeage parsing 实现多线程的语言)。满足以上两个要求的语言不多。 Evan 选择了 Go,我选择了 C++。

减少遍历次数

要想速度快,就要减少 AST 的遍历次数。最好就是只遍历一次来生成代码,在 Parsing 构建 AST 的时候就收集足够的信息。但是这也意味着只能做比较浅层次的优化,不能做深层次的压缩(死代码消除,tree shaking 都做不了)。

架构

由上述思路我总结出了以下打包的架构:

  1. 并行 parse 文件
  2. 作用域提升、生成框架代码、重命名变量
  3. 并行生成代码
  4. 合并输出文件

流程图如下:

分享一款超好用的JavaScript 打包压缩工具

打包压缩原理

本章节主要讲如何“最简单“地压缩 Js 代码。本章节假设读者对编译原理有一定了解,知道什么是 AST。如果不懂请直接跳到下文「性能」章节。

字面量替换

字面替换最简单。规则有一下几个:

  • undefined 替换为 void 0
  • true 替换为 !0 , false 替换为 !1

:warning: 注意:在 ES 中,undefined 是标识符(Identifier),而不是关键字,也就是说你可以定义一个叫 undefined 的变量,所以这个时候不能简单地替换为 void 0

常量折叠

计算简单的运算:

var two = 1 + 1;
var foobar = 'foo' + 'bar';

转换成

var two = 2;
var foobar = 'foobar';

:warning: 注意:这里要注意实现的平台和 js 的差异,比如在 C++ 里面大整数相加可能会溢出,而在 Js 会自动转换成 bigint. 加法问题就如此,其他运算符问题更多。如果要完整实现常量折叠,可能要部分实现 js 引擎。

变量别名

别名就是要给变量重新赋予比较短的变量名。从字母一直排上去,abcd,一个字母用完了用两个字母。实现起来也很简单,用一个计数器,一直加上去就可。最后每个变量分配一个数字,把这个数字映射到相应的英文字母上,有点像 36 进制转换成字母的面试题。不过这里有一点值得注意的是,变量名第一个字母不能是数字,第二个字母开始可以是数字,要考虑到这一点,才能尽可能“压榨”变量名。

为了尽可能地“压榨”变量名,同一级的作用域里面的变量名是可以使用相同的变量名。到下一级的时候,对子作用域进行合并。

举个例子:

function Mother() {
	var e = 'capture'; // d 不能使用跟子作用域同样的变量名,不然子作用域无法捕获这个变量
	function A(a, b, c, d) {
 console.log(e);
	}
	
	function B(a, b, c) { // B 跟 A 函数同级,分配同样的变量名
	 // ...
	}
}

上述例子中,A 和 B 都没有子作用域了,变量名从 0 开始分配。到给 Mother 下 e 分配变量名时,找到子作用域最大的计数器。分配最多的子作用域 A 分配了 4 个,所以 B 计数器从 5 开始分配,所以给 e 分配了5,所以 e 就得到了这个名字。

所以变量别名就是从 AST 的叶子开始向上构造,一直分配到根结点把所有作用域都分配完为止。

小技巧

这里 esbuild 采用了比较聪明的技巧。它统计了所有变量的引用次数,然后进行排序,引用次数最多的变量分配到的名字就是尽量短的,这样也可以减少编译出来 js 的体积。我在写 jetpack 打包的时候,也借鉴了这种做法。

模块合并

模块合并的办法有很多。webpack 采用的是用 function 把每个函数包起来,放到了一个长长的数组里面,然后实现了自己的 require,esbuild 也采用了类似的方法。

Rollup.js 实现的方法则是作用域提升(Scope hoisting),把模块都放到根作用域。这里我采用的方法也是作用域提升。

假设有 a.js 文件:

export function A() {
 console.log('a');
}

然后有 main.js 文件:

import { A as ExternalA } from './a';

function A() {
 console.log('local A');
}

export function main() {
 A() + ExternalA();
}

使用 jetpack 打包完的结果:

// a.js
function A() {
 console.log('a');
}

// main.js
function A_0() {
 console.log('local A');
}

function main() {
 A_0() + A();
}

export { main };

难点在于作用域合并。实际上在 ES modules 里面不同 modules 之间引用是一个图结构。

C++ 的优化

除了策略上的优化,C++ 还提供了诸多基础数据结构/内存方面的优化。

shared_ptr

AST 的结点全部使用 shared_ptr,有人可能认为这是一个很大的开销。但是早期的时候我实现过一个裸指针版本(不释放内存),并没有测出有明显差距。

使用 shared ptr 很重要一个原因是,一个子树可能被其他类拥有(打包模块,Scope,ES Module 管理器)。这个时候如果用 unique ptr 的话就会 gg。只能说 GC 大法好。

对于 C++ 这种没有 GC 的语言有一个毛病就是:析构 AST 非常耗时。AST 够大的话能耗上十几 ms(这个时间跟 gc 比有何优势?),所以因此我也能想出了一个办法: 不释放内存 ……。

最后说一句: GC 大法好

robin hood hashing

由于打包器中大量使用哈希表,所以提高哈希表速度尤其重要,这里我使用了 robin hood hashing

参见: https://martin.ankerl.com/2019/04/01/hashmap-benchmarks-01-overview/

在 hash 方面我有一个设想,就是像 Lua 一样,对于短字符,在字符串创建的时候把 hash 记下来,这样在多次使用哈希表的时候可以节省 hash 的时间(但是要求字符串是 immutable 的)。为此我专门写了个 String 类,最后的结果是总体速度慢了 2-3x,测出来是 immutable 字符串拼接耗时太多,最后放弃了这个方案。

jemalloc

Parsing 过程中需要大量分配 node,大家都知道很明显 C++ 的 new 并不够快。经过测试在 macOS 下使用 jemalloc 会让 parsing 速度提升 1 倍。使 用系统 malloc 会导致 parsing 速度比 Go 慢 1x 左右,慢在 new 。

当然了,内存池我也试过的,测出来速度基本和 jemalloc 一样,所以就直接用 jemalloc 了。

性能

分享一款超好用的JavaScript 打包压缩工具

总结

写编译器需要快速大量产生 node 结点,大量树和图的结构,这一方面的运算 C++ 并没有什么优势可言。

不得不承认,使用 C++ 你要思考很多东西,做很多很多额外的工作,才能获得比 Go 还快的速度(什么都不想做出来只会比 Go 还慢)。另一方面使用 C++ 会让你额外考虑很多和业务无关的东西,大大降低开发速度,而对于打包器这个场景 C++ 在这一块本身不能提供很大优势。

到此这篇关于写一个飞快的 JavaScript 打包压缩工具的文章就介绍到这了,更多相关JavaScript 打包压缩工具内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
js将控件隐藏及display属性的使用介绍
Dec 30 Javascript
JavaScript link方法入门实例(给字符串加上超链接)
Oct 17 Javascript
JS采用绝对定位实现回到顶部效果完整实例
Jun 20 Javascript
微信小程序 视图容器组件的详解及实例代码
Jan 19 Javascript
使用jQuery和ajax代替iframe的方法(详解)
Apr 12 jQuery
详解express与koa中间件模式对比
Aug 07 Javascript
原生js实现省市区三级联动代码分享
Feb 12 Javascript
微信小程序swiper实现滑动放大缩小效果
Nov 15 Javascript
VUE+Element环境搭建与安装的方法步骤
Jan 24 Javascript
vue动态渲染svg、添加点击事件的实现
Mar 13 Javascript
详解Nuxt内导航栏的两种实现方式
Apr 16 Javascript
Vue OpenLayer 为地图绘制风场效果
Apr 24 Vue.js
微信小程序自定义navigationBar顶部导航栏适配所有机型(附完整案例)
Apr 26 #Javascript
javascript 使用sleep函数的常见方法详解
Apr 26 #Javascript
基于JavaScript实现十五拼图代码实例
Apr 26 #Javascript
小程序自定义导航栏兼容适配所有机型(附完整案例)
Apr 26 #Javascript
vue 使用 vue-pdf 实现pdf在线预览的示例代码
Apr 26 #Javascript
javascript设计模式 ? 访问者模式原理与用法实例分析
Apr 26 #Javascript
详解关于Vue单元测试的几个坑
Apr 26 #Javascript
You might like
我的论坛源代码(三)
2006/10/09 PHP
php Http_Template_IT类库进行模板替换
2009/03/19 PHP
php获取服务器端mac和客户端mac的地址支持WIN/LINUX
2014/05/15 PHP
php 购物车完整实现代码
2014/06/05 PHP
微信公众平台之快递查询功能用法实例
2015/04/14 PHP
PHP设计模式之适配器模式原理与用法分析
2018/04/25 PHP
php实现的表单验证类完整示例
2019/08/13 PHP
PHP中用Trait封装单例模式的实现
2019/12/18 PHP
不使用XMLHttpRequest实现异步加载 Iframe和script
2012/10/29 Javascript
js利用事件的阻止冒泡实现点击空白模态框的隐藏
2014/01/24 Javascript
JS图片自动轮换效果实现思路附截图
2014/04/30 Javascript
javascript实现网页屏蔽Backspace事件,输入框不屏蔽
2015/07/21 Javascript
JavaScript构造函数详解
2015/12/27 Javascript
AngularJS 自定义过滤器详解及实例代码
2016/09/14 Javascript
用jmSlip编写移动端顶部日历选择控件
2016/10/24 Javascript
AugularJS从入门到实践(必看篇)
2017/07/10 Javascript
基于Vue的SPA动态修改页面title的方法(推荐)
2018/01/02 Javascript
Node中使用ES6语法的基础教程
2018/01/05 Javascript
React+Antd+Redux实现待办事件的方法
2019/03/14 Javascript
Vue源码之关于vm.$delete()/Vue.use()内部原理详解
2019/05/01 Javascript
微信JSSDK实现打开摄像头拍照再将相片保存到服务器
2019/11/15 Javascript
javascript实现弹出层效果
2019/12/10 Javascript
python里大整数相乘相关技巧指南
2014/09/12 Python
Python中装饰器学习总结
2018/02/10 Python
Python结合百度语音识别实现实时翻译软件的实现
2021/01/18 Python
美国在线工具商店:Acme Tools
2018/06/26 全球购物
法国在线购买汽车轮胎网站:123pneus.fr
2019/02/25 全球购物
美国在线轮胎零售商:SimpleTire
2019/04/08 全球购物
俄罗斯三星品牌商店:GalaxyStore
2020/11/04 全球购物
写给女朋友的检讨书
2014/01/28 职场文书
高中生第一学年自我鉴定
2014/09/12 职场文书
资源环境与城乡规划管理专业自荐书
2014/09/26 职场文书
Spring Data JPA的Audit功能审计数据库的变更
2021/06/26 Java/Android
Java生成日期时间存入Mysql数据库的实现方法
2022/03/03 Java/Android
Golang日志包的使用
2022/04/20 Golang
JS实现页面炫酷的时钟特效示例
2022/08/14 Javascript