分享一款超好用的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 相关文章推荐
javascript编程起步(第二课)
Feb 27 Javascript
jquery text()要注意啦
Oct 30 Javascript
初识JQuery 实例一(first)
Mar 16 Javascript
JS自动适应的图片弹窗实例
Jun 29 Javascript
jquery(hide方法)隐藏指定元素实例
Nov 11 Javascript
JQuery zClip插件实现复制页面内容到剪贴板
Nov 02 Javascript
微信小程序 wx.uploadFile在安卓手机上面the same task is working问题解决
Dec 14 Javascript
使用canvas及js简单生成验证码方法
Apr 02 Javascript
简单实现jQuery上传图片显示预览功能
Jun 29 jQuery
了解前端理论:rscss和rsjs
May 23 Javascript
vue实现公告栏文字上下滚动效果的示例代码
Jun 16 Javascript
使用vue构建多页面应用的示例
Oct 22 Javascript
微信小程序自定义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
php解析html类库simple_html_dom(详细介绍)
2013/07/05 PHP
PHP实现单例模式最安全的做法
2014/06/13 PHP
PHP轻量级数据库操作类Medoo增加、删除、修改、查询例子
2014/07/04 PHP
PHP实现的简单组词算法示例
2018/04/10 PHP
Javascript在IE或Firefox下获取鼠标位置的代码
2009/12/18 Javascript
ie下动态加态js文件的方法
2011/09/13 Javascript
JavaScript高级程序设计 阅读笔记(十七) js事件
2012/08/14 Javascript
前台js调用后台方法示例
2013/12/02 Javascript
JavaScript 实现简单的倒计时弹窗DEMO附图
2014/03/05 Javascript
jQuery实现垂直半透明手风琴特效代码分享
2015/08/21 Javascript
jQuery轮播图效果精简版完整示例
2016/09/04 Javascript
javascript事件的绑定基础实例讲解(34)
2017/02/14 Javascript
vue-cli启动本地服务局域网不能访问的原因分析
2018/01/22 Javascript
Vue通过for循环随机生成不同的颜色或随机数的实例
2019/11/09 Javascript
javascript设计模式 ? 原型模式原理与应用实例分析
2020/04/10 Javascript
解决谷歌搜索技术文章时打不开网页问题的python脚本
2013/02/10 Python
深入了解Python中pop和remove的使用方法
2018/01/09 Python
python3.6使用tkinter实现弹跳小球游戏
2019/05/09 Python
Python英文文章词频统计(14份剑桥真题词频统计)
2019/10/13 Python
Python常用模块logging——日志输出功能(示例代码)
2019/11/20 Python
在OpenCV里使用Camshift算法的实现
2019/11/22 Python
PythonPC客户端自动化实现原理(pywinauto)
2020/05/28 Python
CSS3 倾斜的网页图片库实例教程
2009/11/14 HTML / CSS
CSS3 :not()选择器实现最后一行li去除某种css样式
2016/10/19 HTML / CSS
美国眼镜网站:EyeBuyDirect
2017/04/13 全球购物
请解释接口的显式实现有什么意义
2012/05/26 面试题
酒店管理毕业生自荐信
2013/10/24 职场文书
中学教师教育感言
2014/02/21 职场文书
教育英语专业毕业生的求职信
2014/03/13 职场文书
老师对学生的寄语
2014/04/09 职场文书
优秀护士先进事迹
2014/05/08 职场文书
大学生活动总结模板
2014/07/02 职场文书
乡镇遵守党的政治纪律情况对照检查材料
2014/09/26 职场文书
一份教室追逐打闹的检讨书
2014/09/27 职场文书
房屋出售授权委托书
2014/10/12 职场文书
jdbc使用PreparedStatement批量插入数据的方法
2021/04/27 MySQL