Day05 X Code Minimize & Uglify

从今天开始终於要正式进入介绍前端效能优化各种技巧的章节了,如果到今天还愿意继续坚持看下去的读者记得给自己一些掌声 ? 如果对效能优化几乎是零基础的读者也别担心,我会从比较简单的概念开始讲起。好了,废话不多说,马上开始今天的内容吧!

Code Minimize & Uglify

有时候你会想看看一些网页的原始码是怎麽运作的,不过当点选「检查网页原始码」後,显示出来的 code 有时却让你不知道这到底是哪个星球的程序语言,例如检查 Google 首页的网站原始码你会看到

顿时你发现你想进 Google 工作还早个一万年,原来我必须学会撰写这样让人完全看不懂的程序码,才有进入谷歌的门票吗?

失望的我打算看看自己之前写的练习用网站的网页原始码

Oh my God! 我之前在开发的时候并不是这样写的啊!怎麽也变成连自己也看不懂的程序码了?难道我突然获得了能进入 Google 的能力了?(误)

其实这些看起来混乱的程序码其实就是我们开发时写出来的程序,虽然变数名称跟逻辑似乎都跟我们原本开发时写的不一样,但它其实只是经过转译罢了。这个过程中主要的行为有两个:

  • Minimization
  • Uglify

What is minimization and uglify in JavaScript ?

Minimization 指的是以程序的功能不受到影响为前提从 source code 中移除不必要的字元的过程。所谓不必要的字元有空白键 whitespace、注释 comment、分号 semicolon...等。另外还可以将原本名字很长的变数或函式名称、参数替换成简短的字元,用意在於尽量降低档案的大小,这样牺牲程序可读性来换取较低的档案大小的方式又被称作 Uglify,Uglify 除了替换变数名,通常还会打乱程序的逻辑,例如改变原本函式的顺序,避免自家产品的 code 轻松的被别人拿去研究或抄袭,我们来看看实际例子。

例如以下这段 code

const iterations = 50;
const multiplier = 1000000000;

function calculatePrimes(iterations, multiplier) {
  var primes = [];
  for (var i = 0; i < iterations; i++) {
    var candidate = i * (multiplier * Math.random());
    var isPrime = true;
    for (var c = 2; c <= Math.sqrt(candidate); ++c) {
      if (candidate % c === 0) {
          // not prime
          isPrime = false;
          break;
       }
    }
    if (isPrime) {
      primes.push(candidate);
    }
  }
  return primes;
}

function doPointlessComputationsWithBlocking() {
  var primes = calculatePrimes(iterations, multiplier);
  pointlessComputationsButton.disabled = false;
  console.log(primes);
}

经过 JavaScript Minifier 最小化後会变成

const iterations=50,multiplier=1e9;function calculatePrimes(t,i){for(var o=[],a=0;a<t;a++){for(var n=a*(i*Math.random()),r=!0,e=2;e<=Math.sqrt(n);++e)if(n%e==0){r=!1;break}r&&o.push(n)}return o}function doPointlessComputationsWithBlocking(){var t=calculatePrimes(iterations,multiplier);pointlessComputationsButton.disabled=!1,console.log(t)}

可以看到有些数字的表示方式、变数与参数名称都被修改成更简短的字元了,让程序码可读性提昇的字元例如空白、分号也被移除,目的就是为了最大限度的降低字元的数量、降低档案的大小。不过经过最小化後的程序功能是不会改变的,电脑也还是有办法读得懂压缩後的程序码。

上述的例子在经过 Minimization 前後的 file size 其实并不会差多少,不过当档案本身很肥大的时候,Minimization 与 Uglify 所带来的载入效能增长也是不可忽略的喔!

如果要试试看效果,可以参考如 JavaScript MinifierUglify JS 等网页服务。

网页应用里,只有 JS 能做 Minimization ?

当然不是,大家熟悉的网站三剑客「HTML、CSS、JavaScript」都可以做 Minimization 来降低档案大小喔!

HTML Minifier
CSS Minifier

闪开点,让专业的来。

到这里我们了解 Code Minification 与 Uglify 的重要性了,但是难道为了效能,我们必须这样一直手动把 source code 丢给 Minification 的工具再进行部署吗?闪开点,让专业的 bundler 例如 gulp、Webpack 来自动处理这些事吧!

举例来说,要使用 webpack 打包程序码并经过最小化可以透过设定 bundler 的 config 并搭配一些 plugins 就能轻松达成

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ]
}

如果使用的是 gulp ,则会类似这样:

// gulpfile.js

const gulp = require('gulp');
const uglify = require('gulp-uglify');

gulp.task('watch', function(event) {
    const watcher = gulp.watch('client/js/*.js');
    watcher.on('change', function(event) {
    console.log('file: ' + event.path + 'changed!');
    });
})

gulp.task('uglify', function() {
    gulp.src('client/ks/**/*.js')
        .pipe(uglify())
        .pipe(gulp.dest('client/dist'));
})

gulp.task('default', function(){
    console.log('gulp is running...');
})

在终端机跑 gulp 指令後

gulp

就可以得到 minifized 後的 code。

其实 Code Minification 与 Uglify 已经是在使用各种 bundler 时一定会搭配使用的基本功能了,如果你使用的是 create-react-app 这种 template 或是 Next.js 这种完整的框架,基本上它们都已经把 Minification 与 Uglify 放到打包时的预设行为里了,所以不用做其他设定就会自动启用了。

读者现在应该也可以理解为什麽文章开头贴的练习用网站的原始码会长那样了,因为网站程序码在部署前已经透过打包工具执行 Minimize 与 Uglify 啦!

我就问,这样怎麽 debug ?

有读者可能会问,那这样开发者怎麽 debug ? 如果程序遇到错误根本没办法去 trace source code 啊!的确,这是 minimization and uglify 的缺点之一,光看编译後的 code 根本没办法对应到原本开发时写的程序码。幸好在 2011 年,sourcemap 的规范出现了。source map 就是储存了原始码与编译後程序码的对应关系之档案,让你在开启浏览器 devtool 时,能让浏览器透过载入 source map 的方式帮助你定位原始码位置,方便下中断点 debug。

通常 source map 档案大概会长得像这样子

JSON 物件中各属性代表的意义如下

  • version: 说明 source map 档案的版本。
  • file: source map档案的名称。
  • sources: 一组包含原始档 url 的阵列。
  • sourceRoot: sources属性中那些 url 相对路径的根目录。
  • names: 包含原始档中所有变数和函式名称的阵列。
  • mappings: 一组包含实际程序码对映的Base64 VLQs的字串

看不懂也没关系,记得刚刚说的吗?它只是用来「储存原始码与编译後程序码的对应关系」的档案,所以浏览器看得懂就好。

要产生 source map 也很简单,我们不需要自己去撰写它,现在几乎所有 bundler 都可以透过 config 自动产生 source map 了,不过缺点就是 build time 会更长一点,不过浏览器是当打开 devtool 的时候,才会根据它获取的 source map url 资讯来载入 source map,因此并不会影响网站载入的效能与使用者的体验。

除了 bundler 以外,我们可以试试看用 UglifyJS 的 CLI 工具在将档案做 minimize 的同时也产生对应的 source map 档案:

uglifyjs [input files] -o script.min.js --source-map script.js.map --source-map-root http://example.com/js -c -m
  • --source-map - 生成的source map档案的名称。
  • --source-map-root - (Optional)指定 source map 档案中 sourceRoot 属性的值。
  • --source-map-url - (Optional)在经过优化的压缩档案底部的注释中指定source map档案的路径。如://# sourceMappingURL=/path/to/script.js.map
  • --in-source-map - (Optional)输入的 source map。假如你正在压缩的 JavaScript 档案是从另一个原始档生成的。

执行完之後在经过压缩的档案尾端会出现一串识别字串用来识别这个档案的 source map 档案的路径,例如

... some minimized script

//# sourceMappingURL=/path/to/script.js.map

当浏览器的 Devtool 被开启时,如果 source maps 功能有被启用,那麽该路径的 source map 档案将会被载入。

另一种方式则是在压缩的 JavaScript 档案的 Response 传送 X-SourceMap HTTP header 来指定 source map

X-SourceMap: ../some path/script.js.map

最後还要确保浏览器是有开启 source map 功能的,以 Chrome 来说,到 Devtool 的设定开启 JavaScript 与 CSS 的 source map

点选 Devtool 的 source tab 时如果浏览器找到 source map 的档案,就会显示 mapping 过後的 code 罗!

话说 source map 背後运作的机制还蛮有趣的,不过因为跟本系列文比较无关,就留给读者自行研究罗,有兴趣的读者可以参考这篇文章

数字比较会说话,来看看 benchmarks 吧!

如果只跟各位说经过 Minification 後档案大小会变小,好像没什麽说服力,能不能来点数字对比啊!毕竟变小 1KB 也是变小啊!那就来看看社群上的大大做的各个 Minifiers 的效果对比吧!

Github Repo: https://github.com/privatenumber/minification-benchmarks

以大家都听过的 react 为例子,在进行 minifiy 以前的 size 大约是 72.1 kB(Gzip 档案压缩会在之後的章节介绍,今天就先不讨论),经过 minifier 最小化後档案大小可以最多缩小一半以上,这对载入效能绝对有很大的改善,透过数字对比,各位读者应该了解 minimization 与 uglify 的强大了吧!

如果想试试看使用 minifier 的话,还蛮推荐大家去玩玩看 esbuild 的,bundle 与 minify 的速度真的很快很快,可以写写简单的程序去体验看看喔!

import esbuild from 'esbuild';

export default async ({ code }) => {
  const minified = await esbuild.transform(code, {
    minify: true,
    sourcemap: false,
    legalComments: 'none',
  });

  return minified.code;
};

本日小结

今天的内容自己觉得蛮简单的,在开发时基本上使用 bundler 就可以轻松地做到程序码的最小化,这也是现今 web 开发基本上一定会做的优化,是非常重要的概念。谈完了程序码档案大小的优化,明天来谈谈在一个网页中占很大比例且不可或缺的要素--图片的各种优化方式。

References

https://blog.techbridge.cc/2021/03/28/how-source-map-works/
https://esbuild.github.io/api/#minify
https://www.imperva.com/learn/performance/minification/
https://github.com/privatenumber/minification-benchmarks


<<:  前端工程师也能开发全端网页:挑战 30 天用 React 加上 Firebase 打造社群网站|Day20 会员选单

>>:  [Day - 05] - Spring Bean 运作与原理

30天学会C语言: Day 19-考试常用的输入格式

多行输入 or 单行多个数值 如果输入的行数或一行中输入的数值数量固定且非常多,可以用回圈达成 #i...

[第七天]从0开始的UnityAR手机游戏开发-介绍Unity的Asset Store和从Asset Store或网路下载3D模型

介绍Unity资源商店 先开启Asset Store,将Assets Store拖曳出来 对着As...

[Day7] Local File Inclusion / Remote File Inclusion

前言 中场休息过後,来看一下LFI和RFI吧! 正文 LFI LFI全称Local File Inc...

[Day16] - 利用 direflow.io 将 React Component 转换成 Web Component

昨天解说 Vue 如何制作 Web-Component 今天来说明一下 , 那 React 如何制作...

DAY17-EXCEL统计分析:T检定介绍

T检定: 什麽时候会用到T检定呢? 当我们不知道母体变异数为多少时就是用T检定 T检定的公式为: (...