前端构建是指通过工具自动化地处理那些繁琐、重复而有意义的任务。
  这些任务包括语言编译、文件压缩、模块打包、图像优化、单元测试等一切需要对源码进行处理的工作。
  在将这类任务交给工具后,开发人员被解放了生产力,得以集中精力去编写代码业务,提高工作效率。
  构建工具从早期基于流的[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 是打包首选。
:-: ![](https://img.kancloud.cn/22/0a/220ab945a9988d058270a30ecef5880d_1364x489.png =800x)
**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 等。
:-: ![](https://img.kancloud.cn/ee/0c/ee0cdbdec497af0f64215036f050adcc_3224x1835.png =800x)
  这类比较大的包可以再单独剥离,不用全部聚合在 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,如下图所示。
:-: ![](https://img.kancloud.cn/60/e2/60e2b9c4afbb83c585273aee6f9afd63_3224x1836.png =800x)
  其实有时候只是使用了开源库的一个小功能,若不复杂,那完全可以自己用代码实现,这样就不必依赖那个大包了。
  例如常用的[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 默认的路由加载是在打包时,将所有模块合并到一个文件中,首次进入时加载这个包,后续的路由切换就不需要重新进行网络请求了。
:-: ![](https://img.kancloud.cn/96/77/9677cbd9175cfd9c9006f7ac4892e5ee_1396x806.png =600x)
  所以这种方式将网络瓶颈都给了首屏,为了加速首屏的呈现,可以将不同路由对应的模块分割成不同的代码块,然后当路由被访问的时候才加载对应模块。
  这就是路由懒加载的执行过程,其实就是个分包和分请求的过程,加载压力也分散到了各个路由中。
  例如原先所有的脚本都打包在 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 模块标准管理源码,也就是让浏览器接管了打包程序的部分工作,在页面进行路由时按需提供源码(路由懒加载)。
:-: ![](https://img.kancloud.cn/b3/91/b391eafe8494db12c4601f6cf9d83acb_1390x804.png =600x)
## 总结
  在构建之前,也可以做一些前置优化。
  例如对浏览器兼容性要求不高的场景,可以将编译脚本选择 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),欢迎阅读。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[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年
- 日志
- 2020