趣玩 JavaScript 性能优化工具 Facebook Prepack

Prepack

Prepack 是 Facebook 最近开源的一个 JavaScript 代码优化工具,它跟 Babel、谷歌的 Closure Compiler 类似,运行在 “编译” 阶段,生成优化后的代码。(实际上 Prepack 的源代码生成使用的就是 Babel)

它最基本能力就是四个字:计算消除。

看一个官方的例子代码:

源代码:

(function () {
            function hello() { return 'hello'; }
            function world() { return 'world'; }
            global.s = hello() + ' ' + world();
          })();
          

优化后:

(function () {
            s = "hello world";
          })();
          

可以发现,hello 和 world 函数在编译优化时就已经被执行并替换成计算结果了,这样在运行时就没有函数调用和计算开销,即:“计算消除” 。

当然,这个例子非常 “愚蠢”,没有人会写这种代码。因为这仅仅是个展示能力的例子而已,如果你理解了,就应该知道函数内部计算过程复杂和简单最终都会被替换成计算结果,没有区别。

我们再来看个计算过程复杂的(斐波那契函数):

源代码:

(function () {
            function fibonacci(x) {
              return x <= 1 ? x : fibonacci(x - 1) + fibonacci(x - 2);
            }
            global.x = fibonacci(23);
          })();
          

优化后:

(function () {
            x = 28657;
          })();
          

😅 是不是明白了。

Prepack & Closure Compiler

恰好最近还发现一篇文章,一个老外企图组合 Prepack 和 Closure Compiler:http://www.syntaxsuccess.com/viewarticle/combining-prepack-and-closure-compiler

才发现确实有些有趣啊,我们创建 week.js,填充代码:

(function () {
              function getWeekWords() {
                  return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
              }
          
              function getWeekNumbers() {
                  return [0, 1, 2, 3, 4, 5, 6];
              }
          
              var words = getWeekWords();
              for (var i = 0; i < words.length; i++) {
                  if (words[i] === 'Monday') {
                      console.log(words[i]);
                  }
              }
          
              var numbers = getWeekNumbers();
              for (var i = 0; i < numbers.length; i++) {
                  if (numbers[i] === 1) {
                      console.log(numbers[i]);
                  }
              }
          })();
          

Closure Compiler 编译结果:

for (var n = "Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),
                   r = 0; r < n.length; r++)"Monday" === n[r] && console.log(n[r]);
          for (n = [0, 1, 2, 3, 4, 5, 6], r = 0; r < n.length; r++)1 === n[r] && console.log(n[r])
          

Prepack 编译结果:

(function () {
              function _0() {
                  return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
              }
          
              function _1() {
                  return [0, 1, 2, 3, 4, 5, 6];
              }
          
              getWeekWords = _0;
              getWeekNumbers = _1;
              i = undefined;
              numbers = undefined;
              words = undefined;
              words = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
              i = 0;
              i = 1;
              console.log("Monday");
              i = 2;
              i = 3;
              i = 4;
              i = 5;
              i = 6;
              i = 7;
              numbers = [0, 1, 2, 3, 4, 5, 6];
              i = 0;
              i = 1;
              console.log(1);
              i = 2;
              i = 3;
              i = 4;
              i = 5;
              i = 6;
              i = 7;
          })();
          

Closure Compiler 优化的是代码体积,并不会考虑代码中的性能因素而进行特殊优化。可以看到中间多余变量赋值和函数调用都被优化掉了,变成了循环两个字面值数组。

而 Prepack 是性能优化工具,它根本不在意代码体积,你甚至可以极其轻易的写出会导致代码膨胀的编译结果,例如:

var array = Array.apply(null, new Array(9999)).map((item, i) => i);
          

会生成 9999 个字面值创建的数组 array 的代码(可想而知代码会膨胀得多厉害):

array = undefined;
          array = [0, 1, 2, 3, <...>, 9999]
          

我们抛开这段没用的会导致膨胀的循环申请数组代码,看上面的 week.js 的结果,会发现 Prepack 在执行函数内的计算步骤时,将展开循环后的每一次结果赋值都作为代码输出了。哪些是有用的哪些是冗余没用的,Prepack 是 “不知道” 的,它所知道的是每一次赋值都是循环展开中的行为。

那么… 这不是典型的 Closure Compiler 优化场景么!如果我们先将 Prepack 的编译结果输出给 Closure Compiler 编译一遍的话:

console.log("Monday"),console.log(1);
          

得到了一个最完美的结果!

附加配置

如果你要组合这两个东西到 Webpack 中:

const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
          const ClosureCompilerPlugin = require('webpack-closure-compiler');
          const webpack = require('webpack');
          const path = require('path');
          const buildPath = path.resolve(__dirname, 'dist');
          const srcPath = __dirname + '/src';
          const configuration = {};
          
          module.exports = {
              entry: {
                  result: srcPath + '/entry.js'
              },
              output: {
                  path: buildPath,
                  filename: '[name].js'
              },
              resolve: {
                  extensions: ['.js']
              },
              module: {
                  loaders: []
              },
              plugins: [
                  new ClosureCompilerPlugin({
                      compiler: {
                          language_in: 'ECMASCRIPT6',
                          language_out: 'ECMASCRIPT5',
                          compilation_level: 'ADVANCED'
                      },
                      concurrency: 3,
                  }),
                  new PrepackWebpackPlugin(configuration)
              ]
          };
          

最后

Prepack 目前来看是非常初期的阶段,官方对它也有非常浩大的展望,但是究竟这个项目是否能达到目的,还不好说。详情可以看官网最下面的 Roadmap 。(也可以看知乎上的相关中文回答)

不过有两点需要指出:Prepack 的优化目标不仅仅是计算消除,和 Closure Compiler 混用也是娱乐用途,用在真实项目上要解决的问题肯定不少。而 Prepack 目前还有许多问题,也不建议用在生产项目上。