# Performance 組建流程優化 Build Process Optimization- Code Splitting 在開發流程中我們會使用到不同的工具,在組建流程我們會使用到 Bundler 這個工具。 > What is bundler? Why we need bundler? 那先來 Javascript 的 Module 的歷史吧~ 會在這個過程中發現 bundle 是怎麼開始的~ # **A History of JavaScript Modules and Bundling** ### In the Beginning, Multi-Page Websites With Global Scripts 當 user 訪問不同網頁路徑時,user 會向 server request 這一個網頁路徑的靜態語言(HTML+CSS+JS),不同的路徑會有各自的程式碼。 當時我們會寫多個 js 檔,以 script tag 的方式引入。 ![Pasted 2024-03-11-11-12-24](https://hackmd.io/_uploads/BkMqoD36p.png) 像是上的的程式碼,`main.js` 依賴 `calculate.js`, `calculate.js` 依賴 `add.js`,script 必須按照正確的順序,而且變數都被加到 **global namespace**! 全域的變數很容易被修改或是混淆。 它們還是 synchronous and blocking,如下圖當 user 端訪問網頁後,瀏覽器在執行 script tag 那一行時,它會發送另一個 request,從 server 下載 `js` 檔。執行 HTML頁面的過程會暫停,直到 script 下載完成。 ![截圖 2024-03-10 晚上8.09.30](https://hackmd.io/_uploads/H1Sx2v2TT.png) 在 Bundling 時代開始前,有了一些解決方案,但只是 Workarounds 並不是真的解決的問題。 像是 “Module Pattern" (IIFE), 或是`jQuery` 的 `$` 符號。 + IIFE, Immediately Invoked Function Expression Module Pattern provide a way to create private variables and expose a controlled set of public methods or properties. ```javascript var shoppingCartModule = (function() { // Private variables var cart = []; // Private functions function addToCart(item) { cart.push(item); console.log("Added to cart: " + item); } function removeFromCart(item) { var index = cart.indexOf(item); if (index !== -1) { cart.splice(index, 1); console.log("Removed from cart: " + item); } else { console.log(item + " not found in cart."); } } // Public API return { addItem: function(item) { addToCart(item); }, removeItem: function(item) { removeFromCart(item); }, getCart: function() { return cart.slice(); // Return a copy of the cart }, getTotalItems: function() { return cart.length; } }; })(); ``` ## And Then Came Bundling 為了模組化,開發者將程式碼分成多個檔案, 這樣的結果是 browser 會向 server request 多個小的 js 檔。那何不把所有的 js 檔 Bundle 在一起,這樣 browser 只需要 request 一個 js 檔! 剛開始開發者會手動將檔案 `cat`\-ing 在一起,有麻煩事的時候就會解答~ 隨後出現了 minifying 或 uglifying 的工具,JSMin from Douglas Crockford. ## **Node's Modules Introduce Backend Runtime** 2009年,Ryan Dahl 創建了 Node,像其他語言一樣,Node 需要一個packaging system。這個 packaging system 使不同的 package(programs)能夠被共享並被其他人使用,很快地,大家開始製作 code package, 這些 code package 也就是 node modules,而 `npm` 被創建用於管理這些 node modules。 ```javascript var otherModule = require('./otherModule'); ``` ```javascript module.exports = MyModule; ``` 其中透過 CommonJS 的標準來實踐,⚠️ JavaScript 本身並不原生支持require/module.exports syntax,Node compiler 和 runtime 能夠讀取並執行它們。同時 Browsers 也不懂這個 syntax,不能在 JavaScript 中使用後端 Node module libraries。 ### **Modules For the Browser - AMD** 前端羨慕嫉妒恨,Node 有自己的 module system,也為瀏覽器創建了可以解析執行 module syntax 的工具。其中最著名的是 [RequireJS](https://requirejs.org/), Asynchronous Module Definition (AMD) system 非同步模組定義, ```javascript define(['calculate, alert'], function(calculate, alert){ var values = [ 1, 3, 9 ]; var calculation = calculate(values) alert('Hello') document.getElementById("calculation").innerHTML = calculation; }) ``` FYI, 在 modern HTML,`<script>` 有也 asynchronous imports the [`async` or `defer` script attributes](https://levelup.gitconnected.com/html-script-element-attributes-async-vs-defer-vs-type-module-610b50a79dbd).) AMD module syntax 已經是一大進步了,我們不用在手動的組織依賴關係。\ 但!是!問題又來了~ 1. Browser 為每一個 dependency 向 serever 發送 requests,dependency 的 dependency…等,很多很多小的 server requests. 2. RequireJS 僅適用於 AMD 語法,不適用於 CommonJS/Node 語法,所以很多 Node modules,frontend 沒辦法使用,所以必須要寫不同的兩個版本,用 if/else 去判斷環境 (Node or browser)。 > 救星也來了~ Browserify 引入 bundling software 來解決這兩個問題~ ### **Browserify - Bundling Node Modules For Browsers** 他讓前端可以使用 CommonJS `require/export` syntax,並使用他們所需要的任何 node modules!在 server 執行 `build` 後,Browserify 會將所有的 `require` statements,通過 dependency paths 進行爬取(包含 Node modules),並建構出一個 bundle 檔(一大包沒有 require statements 的 js 檔),讓瀏覽器可以直接讀取。 他還將 Node built-ins (基本上是整個 library) 加上 polyfills 和 shims(replacement JS code),來兼容每個瀏覽器和瀏覽器版本。 `<script src="bundle.js"></script>` > 現在我們來到了 modern JavaScript bundling world~\ > 執行一個 bundler program,然後 Build Process 中優化它! ## Webpack and Single-Page Web Apps Webpack 在 2015年創建,並被 React 採用,它迅速取代 Browserify。 ![截圖 2024-03-11 下午1.11.30](https://hackmd.io/_uploads/r1F6iwhpT.png) 官網:Webpack is a bundler for modules. The main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset 他為網站所有的資源(JS, CSS, images, SVGs, HTML) 創建了一個 dependency graph,將資源打包成一個或多個綜合性的文件。 就像 Browserify 一樣,它可以能夠解析使用 AMD 或 CommonJS syntax 的 js,創建出 bundle 檔,再從 HTML 中使用`<script src=bundle.js></script>`. SPA (React)崛起,從原本的根據每個路徑像 server 發 request, 或是根據 dependency。現在, 整個 App 會在 user 第一次 request 時被下載,這意味著 App 第一次加載和響應的時間很長! ![image](https://hackmd.io/_uploads/r1Dhiv36a.png) 那重點來了!問題來了~救星也來了~終於來到優化組建流程了! # Build Process Optimization 試想,\ 當我們只是改一行程式碼,但整個 App 和所有的 dependencies 都再重新 ship 給使用者,即使有些 dependencies 不常變動? 或是 user 只造訪 A 頁面,但 B,C,D 的程式碼也都被載入了! ## Code splitting **MDN: Code splitting** is the practice of splitting the code a web application depends on — including its own code and any third-party dependencies — into separate bundles that can be loaded independently of each other. **This allows an application to load only the code it actually needs at a given point in time, and load other bundles on demand.** This approach is used to improve application performance, especially on initial load. 簡單來說,需要的時候再載入就好! # Code splitting third party 開發中,我們會在不同畫面中使用相同的程式碼 (libraries, frmework,.. ) 這些是你的 vendor bundle,有可能是需要預先載入的程式碼,例如 lodash, react,… (common enough that every single gonna buy it),可以將這些 vendor 打包成另一個 chuck。 拆出 Vendor Bundle 的好處是,因為它變動的頻率相對較低,可以被 cache,browser只需要載入其他的程式碼,加快了網站的載入速度。控制 chuck 的命名是重要的! ![截圖 2024-03-11 下午2.34.54](https://hackmd.io/_uploads/ryJQnD2ap.png) + Webpack:把所有的 node_modules 打包成一包 myVendors ```javascript // webpack.config.js optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, name: 'myVendors', }, }, }, }, ``` Before: ![截圖 2024-03-11 下午3.33.15](https://hackmd.io/_uploads/B1pL3v26T.png) After: ![截圖 2024-03-11 下午3.32.16](https://hackmd.io/_uploads/HypUhw2pT.png) ## Code splitting with Dynamic Import 範例一:lazy, Suspense, import ```javascript import { useState, lazy, Suspense } from 'react'; import './App.css'; // lazy load component const MyDefaultComponent = lazy(() => import('./MyDefaultComponent')); function App() { const [name, setName] = useState(''); const onLoad = () => { // lazy load utilities import('./names').then((module) => { console.log('module', module); setName(module.default); }); }; const onLoadLodash = async () => { // lazy load packages const { default:_} = await import ('lodash'); setName(_.capitalize(name)); } return ( <> <button onClick={onLoad}>Load Name</button> <button onClick={onLoadLodash}>Load Lodash</button> <div>{name}</div> {name && ( <Suspense fallback={<h1>Loading...</h1>}> <MyDefaultComponent /> </Suspense> )} </> ); } export default App; ``` 範例二:您只需要在 user 登錄後才使用 Firestore ![截圖 2024-03-11 下午6.33.13](https://hackmd.io/_uploads/B15jhwhap.png) 範例三: Route code-splitting ![截圖 2024-03-11 晚上7.36.44](https://hackmd.io/_uploads/rJKChvhpp.png) [`當 bundle 的檔案超過 500 KiB`](https://github.com/vitejs/vite/discussions/9440), Webpack 或 Vite 會提醒開發者! ![截圖 2024-03-11 下午4.46.06](https://hackmd.io/_uploads/S1afoP26a.png) <br> > 開放討論:大家有做 code splitting 嗎?當遇到檔案超過 500 KiB,要不要 code splitting? <br> ![截圖 2024-03-11 晚上7.20.57](https://hackmd.io/_uploads/By4JovhaT.png) Btw, [Gmail for Mobile HTML5 Series: Reducing Startup Latency](https://googlecode.blogspot.com/2009/09/gmail-for-mobile-html5-series-reducing.html) Gmail 在很久以前就在 code splitting 了~....結束