從今天開始終於要正式進入介紹前端效能優化各種技巧的章節了,如果到今天還願意繼續堅持看下去的讀者記得給自己一些掌聲 ? 如果對效能優化幾乎是零基礎的讀者也別擔心,我會從比較簡單的概念開始講起。好了,廢話不多說,馬上開始今天的內容吧!
有時候你會想看看一些網頁的原始碼是怎麼運作的,不過當點選「檢查網頁原始碼」後,顯示出來的 code 有時卻讓你不知道這到底是哪個星球的程式語言,例如檢查 Google 首頁的網站原始碼你會看到
頓時你發現你想進 Google 工作還早個一萬年,原來我必須學會撰寫這樣讓人完全看不懂的程式碼,才有進入谷歌的門票嗎?
失望的我打算看看自己之前寫的練習用網站的網頁原始碼
Oh my God! 我之前在開發的時候並不是這樣寫的啊!怎麼也變成連自己也看不懂的程式碼了?難道我突然獲得了能進入 Google 的能力了?(誤)
其實這些看起來混亂的程式碼其實就是我們開發時寫出來的程式,雖然變數名稱跟邏輯似乎都跟我們原本開發時寫的不一樣,但它其實只是經過轉譯罷了。這個過程中主要的行為有兩個:
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 Minifier 或 Uglify JS 等網頁服務。
當然不是,大家熟悉的網站三劍客「HTML、CSS、JavaScript」都可以做 Minimization 來降低檔案大小喔!
到這裡我們了解 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 ? 如果程式遇到錯誤根本沒辦法去 trace source code 啊!的確,這是 minimization and uglify 的缺點之一,光看編譯後的 code 根本沒辦法對應到原本開發時寫的程式碼。幸好在 2011 年,sourcemap 的規範出現了。source map 就是儲存了原始碼與編譯後程式碼的對應關係之檔案,讓你在開啟瀏覽器 devtool 時,能讓瀏覽器透過載入 source map 的方式幫助你定位原始碼位置,方便下中斷點 debug。
通常 source map 檔案大概會長得像這樣子
JSON 物件中各屬性代表的意義如下
看不懂也沒關係,記得剛剛說的嗎?它只是用來「儲存原始碼與編譯後程式碼的對應關係」的檔案,所以瀏覽器看得懂就好。
要產生 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 檔案的路徑,例如
... 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 背後運作的機制還蠻有趣的,不過因為跟本系列文比較無關,就留給讀者自行研究囉,有興趣的讀者可以參考這篇文章。
如果只跟各位說經過 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 開發基本上一定會做的優化,是非常重要的概念。談完了程式碼檔案大小的優化,明天來談談在一個網頁中佔很大比例且不可或缺的要素--圖片的各種優化方式。
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