前端構建是指通過工具自動化地處理那些繁瑣、重復而有意義的任務。
  這些任務包括語言編譯、文件壓縮、模塊打包、圖像優化、單元測試等一切需要對源碼進行處理的工作。
  在將這類任務交給工具后,開發人員被解放了生產力,得以集中精力去編寫代碼業務,提高工作效率。
  構建工具從早期基于流的[gulp](https://www.gulpjs.com.cn/),再到靜態模塊打包器[webpack](https://webpack.js.org/),然后到現在炙手可熱的[Vite](https://cn.vitejs.dev/),一直在追求更極致的性能和體驗。
  構建工具的優化很大一部分其實就是對源碼的優化,例如壓縮、合并、Tree Shaking、Code Splitting 等。
## 一、減少尺寸
  減少文件尺寸的方法除了使用算法壓縮文件之外,還有其他優化方式也可以減小文件尺寸,例如優化編譯、打包等。
**1)編譯**
  在現代前端業務開發中,對腳本的編譯是必不可少的,例如 ES8 語法通過[Babel](https://www.babeljs.cn/)編譯成 ES5,[Sass](https://sass-lang.com/)語法編譯成 CSS 等。
  在編譯完成后,JavaScript 或 CSS 文件的尺寸可能就會有所增加。
  關于腳本文件,若不需要兼容古老的瀏覽器,那推薦直接使用新語法,不要再編譯成 ES5 語法。
  例如 ES6 的 Symbol 類型編譯成 ES5 語法,[如下所示](https://www.babeljs.cn/repl),代碼量激增。
~~~
let func = () => {
let value = Symbol();
return typeof value;
};
// 經過 Babel 編譯后的代碼
function _typeof(obj) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (obj) {
return typeof obj;
}
: function (obj) {
return obj && "function" == typeof Symbol &&
obj.constructor === Symbol && obj !== Symbol.prototype
? "symbol" : typeof obj;
}),
_typeof(obj)
);
}
var func = function func() {
var value = Symbol();
return _typeof(value);
};
~~~
  為了增加編譯效率,需要將那些不需要編譯的目錄或文件排除在外。
  例如 node\_modules 中所依賴的包,配置如下所示。
~~~
module.exports = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: "babel-loader",
exclude: /node_modules/
},
]
}
};
~~~
**2)打包**
  在 webpack 打包生成的 bundle 文件中,除了業務代碼和引用的第三方庫之外,還會包含管理模塊交互的 runtime。
  runtime 是一段輔助代碼,在模塊交互時,能連接它們所需的加載和解析邏輯,下面是通過 webpack 4.34 生成的 runtime。
~~~
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
~~~
  在代碼中定義了一個加載模塊的函數:\_\_webpack\_require\_\_(),其參數是模塊標識符,還為它添加了多個私有屬性。
  在編寫的源碼中所使用的 import、define() 或 require() 等模塊導入語法,都會被轉換成 \_\_webpack\_require\_\_() 函數。
  也就是說,webpack 自己編寫 polyfill 來實現 CommonJS、ESM 等模塊語法。
  這里推薦另一個模塊打包工具:[rollup](https://www.rollupjs.com/),它默認使用 ESM 模塊標準,而非 CommonJS 和 AMD。
  所以,rollup 打包出的腳本比較干凈([如下所示](https://rollupjs.org/repl)),適合打包各類庫,React、Vue 等項目都是用 rollup 打包。
~~~
import { age } from './maths.js';
console.log(age + 1)
console.log(1234)
// maths.js 文件中的代碼
export const name = 'strick'
export const age = 30
// 經過 rollup 打包后的代碼
const age = 30;
console.log(age + 1);
console.log(1234);
~~~
  目前,支持 ES6 語法的瀏覽器已達到[98.35%](https://caniuse.com/?search=ES6),如下圖所示,若不需要兼容 IE6~IE10 等古老瀏覽器的話,rollup 是打包首選。
:-: 
**3)壓縮**
  目前市面上有許多成熟的庫可對不同類型的文件進行壓縮。
  例如壓縮 HTML 的[html-minifier](https://github.com/kangax/html-minifier),壓縮 JavaScript 的[uglify-js](https://github.com/mishoo/UglifyJS),壓縮 CSS 的[cssnano](https://github.com/cssnano/cssnano),壓縮圖像的[imagemin](https://github.com/imagemin/imagemin)。
  壓縮后的文件會被去除換行和空格,像腳本還會修改變量名,部分流程替換成三目預算,刪除注釋或打印語句等。
  webpack 和 rollup 都支持插件的擴展,在將上述壓縮腳本封裝到插件中后,就能在構建的過程中對文件進行自動壓縮。
  以 webpack 的[插件](https://webpack.js.org/plugins/)為例,已提供了[ImageMinimizerPlugin](https://webpack.js.org/plugins/image-minimizer-webpack-plugin/)、[OptimizeCssPlugin](https://github.com/NMFR/optimize-css-assets-webpack-plugin)、[UglifyjsPlugin](https://github.com/webpack-contrib/uglifyjs-webpack-plugin)等壓縮插件,生態圈非常豐富。
  2023-11-20 但其實并不是所有場景都需要壓縮,需要因地制宜。
  例如自己團隊維護著一個管理后臺系統,而每次構建的時間都比較長,后面就取消了壓縮的命令,馬上就將時間縮短了 3~4 分鐘。
  雖然腳本明顯變大了,但是這套系統都是在 PC 上運行的,目前的網絡帶寬完全能應付這點腳本尺寸。
**4)Tree Shaking**
  Tree Shaking 是一個術語,用于移除 JavaScript 中未被引用的死代碼,依賴 ES6 模塊語法的靜態結構特性。
  在執行 Tree Shaking 后,在文件中就不存在冗余的依賴和代碼。在下面的示例中,ES 模塊可以只導入所需的 func1() 函數。
~~~
export function func1() {
console.log('strick')
}
export function func2() {
console.log('freedom')
}
// maths.js 文件中的代碼
import { func1 } from './maths.js';
func1();
// 經過 Tree Shaking 后的代碼
function func1() {
console.log('strick');
}
func1();
~~~
  Tree Shaking 最先在 rollup 中出現,webpack 在 2.0 版本中也引入了此概念。
**5)Scope Hoisting**
  Scope Hoisting 是指作用域提升,具體來說,就是在分析出模塊之間的依賴關系后,將那些只被引用了一次的模塊合并到一個函數中。
  下面是一個簡單的示例,action() 函數直接被注入到引用它的模塊中。
~~~
import action from './maths.js';
const value = action();
// 經過 Scope Hoisting 后的代碼
(function() {
var action = function() { };
var value = action();
});
~~~
  注意,由于 Scope Hoisting 依賴靜態分析,因此需要使用 ES6 模塊語法。
  webpack 4 以上的版本可以在[optimization.concatenateModules](https://webpack.docschina.org/configuration/optimization/#optimizationconcatenatemodules)中配置 Scope Hoisting 的啟用狀態。
  比起常規的打包,在經過 Scope Hoisting 后,腳本尺寸將變得更小。
## 二、合并打包
  模塊打包器最重要的一個功能就是將分散在各個文件中的代碼合并到一起,組成一個文件。
**1)Code Splitting**
  在實際開發中,會引用各種第三方庫,若將這些庫全部合并在一起,那么這個文件很有可能非常龐大,產生性能問題。
  常用的優化手段是 Code Splitting,即代碼分離,將代碼拆成多塊,分離到不同的文件中,這些文件既能按需加載,也能被瀏覽器緩存。
  不僅如此,代碼分離還能去除重復代碼,減少文件體積,優化加載時間。
  2023-11-20 對于一些大尺寸依賴,比如圖表庫、Ant Design 等,還可以嘗試引入相關 umd 文件,減少編譯消耗。
  也就是生成的編譯結果,可以直接通過 script 元素請求庫的地址。
~~~
<script src="https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js"></script>
~~~
  Vue 內置了一條命令,可以查看每個腳本的尺寸以及內部依賴包的尺寸。
  在下圖中,vendors.js 的原始尺寸是 3.76M,gzipped 壓縮后的尺寸是 442.02KB,比較大的包是 lottie、swiper、moment、lodash 等。
:-: 
  這類比較大的包可以再單獨剝離,不用全部聚合在 vendors.js 中。
  在 vue.config.js 中,配置 config.optimization.splitChunks(),如下所示,參數含義可參考[SplitChunksPlugin](https://webpack.docschina.org/plugins/split-chunks-plugin)插件。
~~~
config.optimization.splitChunks(
{
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
lottie: {
name: 'chunk-lottie',
test: /[\\/]node_modules[\\/]lottie-web[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
},
swiper: {
name: 'chunk-swiper',
test: /[\\/]node_modules[\\/]_swiper@3.4.2@swiper[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
},
lodash: {
name: 'chunk-lodash',
test: /[\\/]node_modules[\\/]lodash[\\/]/,
chunks: 'all',
priority: 3,
reuseExistingChunk: true,
enforce: true
}
}
}
)
~~~
  在經過一頓初步操作后,原始尺寸降到 2.4M,gzipped 壓縮后的尺寸是 308.64KB,比之前少了 100 多 KB,如下圖所示。
:-: 
  其實有時候只是使用了開源庫的一個小功能,若不復雜,那完全可以自己用代碼實現,這樣就不必依賴那個大包了。
  例如常用的[lodash](https://lodash.com/docs/)或[underscore](https://underscorejs.org/),都是些短小而實用的工具方法,只要單獨提取并修改成相應的代碼(參考[此處](https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore)),就能避免將整個庫引入。
  2023-11-20 除了選擇輕量的依賴庫之外,還可以減少依賴庫的數量。
  公司進行了一次從 Vue2 升級到 Vue3 的項目遷移,新項目依賴庫不僅減少了,并且還輕量了。
  對某個日常 PV 在 8W 的常規活動頁進行數據分析發現,白屏 1 秒內占比從 93.2% 提升至 96.3%,首屏 1 秒內占比從 70.5% 提升至 82.1%。
  由此可見,腳本尺寸對頁面性能的影響巨大,優化腳本可以最快速的達到預期的優化效果。
**2)資源內聯**
  資源內聯會讓文件尺寸變大,但是會減少網絡通信。
  像移[動端屏幕適配腳本](https://github.com/amfe/lib-flexible),就比較適合內聯到 HTML 中,因為這類腳本要最先運行,以免影響后面樣式的計算。
  若是通過域名請求,當請求失敗時,整個移動端頁面的布局將是錯位的。
  webpack 的[InlineSourcePlugin](https://github.com/dustinjackson/html-webpack-inline-source-plugin)就提供了 JavaScript 和 CSS 的內聯功能。
  將小圖像轉換成 Data URI 格式,也是內聯的一種應用,同樣也是減少通信次數,但文件是肯定會大一點。
  還有一種內聯是為資源增加破緩存的隨機參數,以免讀取到舊內容。
  隨機參數既可以包含在文件名中,也可以包含在 URL 地址中,如下所示。
~~~html
<script src="/js/chunk-vendors.e35b590f.js"></script>
~~~
  在 webpack.config.js 中,有個 output 字段,用于配置輸出的信息。
  它的 filename 屬性可聲明輸出的文件名,可以配置成唯一標識符,如下所示。
~~~
module.exports = {
output: {
filename: "[name].[hash].bundle.js"
}
};
~~~
**3)路由懶加載**
  2023-11-20 默認的路由加載是在打包時,將所有模塊合并到一個文件中,首次進入時加載這個包,后續的路由切換就不需要重新進行網絡請求了。
:-: 
  所以這種方式將網絡瓶頸都給了首屏,為了加速首屏的呈現,可以將不同路由對應的模塊分割成不同的代碼塊,然后當路由被訪問的時候才加載對應模塊。
  這就是路由懶加載的執行過程,其實就是個分包和分請求的過程,加載壓力也分散到了各個路由中。
  例如原先所有的腳本都打包在 umi.js 中,而在拆分后,就生成了許多個腳本文件。
~~~
dist/vendors.744fbc30.async.js 5.6 MB 1.5 MB
dist/umi.783bf8b4.js 2.9 MB 614.1 KB
dist/p__live__report__chatAudit.4b06356 2.async.js 1.3 MB 366.6 KB
dist/p__live__liveMonitorDetail__.a7a89995.async.js 1.2 MB 348.8 KB
dist/p__live__liveList__.22ebbc86.async .js 1.2 MB 347.4 KB
~~~
**4)Vite**
  2023-11-20 Vite 是一款前端工具鏈,為開發提供極速響應,注意,它不是一個打包器。
  在開發環境,Vite 會使用 ESBuild 預構建依賴。ESBuild 使用 Go 編寫,比用 JavaScript 編寫的打包器預構建依賴快 10-100 倍。
  但是在生產環境,Vite 選擇了 Rollup 作為打包器。
  因為 Vite 目前的插件 API 與使用 ESBuild 作為打包器并不兼容,Rollup 提供了更好的性能與靈活性方面的權衡。
  Vite 以原生 ESM 模塊標準管理源碼,也就是讓瀏覽器接管了打包程序的部分工作,在頁面進行路由時按需提供源碼(路由懶加載)。
:-: 
## 總結
  在構建之前,也可以做一些前置優化。
  例如對瀏覽器兼容性要求不高的場景,可以將編譯腳本選擇 ES6 語法,用 rollup 打包。
  還可以將一些庫中的簡單功能單獨實現,以免引入整個庫。這部分優化后,打包出來的尺寸肯定會比原先小。
  在構建的過程中,可以對文件進行壓縮、Tree Shaking 和 Scope Hoisting,以此來減小文件尺寸。
  在合并時,可以將那些第三方庫提取到一起,組成一個單獨的文件,這些文件既能按需加載,也能被瀏覽器緩存。
  資源內聯是另一種優化手段,雖然文件尺寸會變大,但是能得到通信次數變少,讀取的文件是最新內容等收益。
*****
> 原文出處:
[博客園-前端性能精進](https://www.cnblogs.com/strick/category/2267607.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1610941255021780992)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020