本文将此前混在Vue文中的Webpack工程单独拎出来成章,以此学习,方便日后查看
性能优化 (渲染、请求、打包、代码)
-
性能指标
-
lighthouse
-
优化资源请求
- 图像
- 使用字体图标和svg标签
<style> background-image: url(‘src’) <svg></svg> </style> <img src=’src’>- 使用响应式图像
<img src=“src” srcset=“src 50w,src2 100w”>- 减小构建时的体积 Tree-shaking
- 压缩资源 nginx中开启gzip对资源进行压缩
- 合并请求
- 图像
-
vue性能优化
-
编码优化
懒加载、异步组件、按需加载(babel-import-plugin)
keep-alive
Object.freeze()
拆分组件
数据持久化(防抖、节流)
v-if v-show -
加载性能优化
第三方模块按需引入 element-ui
vue-virtual-sroll-list
vue-lazy -
用户体验
骨架屏 -
SEO优化
预渲染preender-spa-plugin
服务端渲染ssr -
打包
cdn加载第三方模块
抽离公共文件
sourceMap生成 -
缓存、压缩
-
Webpack
- loader
文件转换;webpack只能解析js,loader使可以解析非js文件url-loader/file-loader vue-style-loader/css-loader/less-loader/sass-loader // 样式转换 cache-loader/babel-loader/babel-core // es6转es5 eslint-loader - plugin
扩展webpack的功能(如打包优化、压缩)DefinePlugin // 定义环境变量 production/delvepment VueLoaderPlugin HotModuleReplacementPlugin // 热更新 HtmlWebpackPlugin // 简化html文件创建,设置loading CommonChunkPlugin uglifyjs-webpack-plugin // 通过UglifyES压缩ES6代码 webpack-parallel-uglify-plugin // 多核压缩,提高压缩速度 MiniCssExtractPlugin(npm install mini-css-extract-plugin --save-dev)// 原本打包过后是css-in-js,配置后可将CSS单独打包出来 CompressionWebpackPlugin // 配置需要压缩的 .js/.css ,ngnix配允许加载压缩文件gzip_static on DllPlugin、DllReferencePlugin // 拆包 configureWebpack: (config) => { // 分割代码块,不适合dll的放到这里 config.optimization.splitChunks = {} } BundleAnalyzerPlugin(npm intall webpack-bundle-analyzer --save-dev) // 查看项目一共打了多少包,每个包的体积,每个包里面的包情况 babel-plugin-import // 按需加载
webpack打包原理
根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源,当 webpack 处理程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle
webpack构建流程
- 初始化参数:
从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 - 开始编译:
用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。 - 确定入口:
根据配置中的 entry 找出所有的入口文件。 - 编译模块:
从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。 - 完成模块编译:
在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。 - 输出资源:
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。 - 输出完成:
在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
graph LR
解析入口文件entry-->|"@babel/parser编译"|生成AST抽象语法树-->|"@babel/traveser"|递归遍历查找所有依赖模块-->|"@babel/core、@babel/preset-env"|生成浏览器可执行code-->重写require生成bundle文件
vue-cli结合webpack-cli配置参数输出
npx vue-cli-service inspect --mode development >> webpack.config.development.js
npx vue-cli-service inspect --mode production >> webpack.config.production.js
热更新HMR
vue-cli-serviceserve启动一个开发服务器 (基于 webpack-dev-server),并附带开箱即用的模块热重载 (Hot-Module-Replacement)
vue-cli-service build --report/--report-json会根据构建统计生成报告,它会帮助你分析包中包含的模块们的大小
- 热更新原理
启动本地服务,当浏览器访问资源时做响应
服务端和客户端使用websocket实现长连接
webpack监听源文件的变化,即当开发者保存文件时触发webpack的重新编译
每次编译都会生成hash值、已改动模块的json文件、已改动模块代码的js文件
编译完成后通过socket向客户端推送当前编译的hash戳
客户端的websocket监听到有文件改动推送过来的hash戳,会和上一次对比
一致则走缓存,不一致则通过ajax和jsonp向服务端获取最新资源
使用内存文件系统去替换有修改的内容实现局部刷新
优化指南
目录
1. 优化工具
1.1. 编译进度
1.2. 编译速度
1.3. 打包体积
2. 优化开发
2.1. 自动更新
2.2. 热更新
3. 加快构建速度
3.1. 更新 webpac4 -> 5
3.2. 缓存
3.3. 减少loder、plugin应用范围
3.4. 优化resolve(解析模块) 配置
3.5. 多进程构建
3.6. 区分 production/development 环境
3.7. SourceMap 路径信息
4. 减小打包体积
4.1. 代码压缩(js&css)
4.2. 代码分离(模块&css)
4.3. Tree Shaking 摇树
4.4. CDN(手动上传字体、图片至网络节点)
5. 加快加载速度
5.1. 按需加载
5.2. 浏览器缓存
5.3. CDN 缓存
一、优化效率的工具
准备工作:安装webpack插件,助力分析
- progress-bar-webpack-plugin 查看编译进度
- speed-measure-webpack-plugin 查看编译速度
- webpack-bundle-analyzer 打包体积分析
1. 编译进度
安装:
npm i -D progress-bar-webpack-plugin
webpack.common.js 配置:
const chalk = require('chalk')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
module.exports = {
plugins: [
// 进度条
new ProgressBarPlugin({
format: `:msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`
})
],
}
效果:
2. 编译速度
安装:
npm i -D speed-measure-webpack-plugin
webpack.dev.js 配置:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ...webpack config...
})
效果:
3. 打包体积
安装:
npm i -D webpack-bundle-analyzer
webpack.prod.js 配置:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
// 打包体积分析
new BundleAnalyzerPlugin()
],
}
效果:
二、优化开发
1. 自动更新
在每次编译代码时,手动运行 npm run build 会显得很麻烦。
webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码:
webpack官方推荐的是 webpack-dev-server
2. 热更新
热更新 指的是,在开发过程中,修改代码后,仅更新修改部分的内容,无需刷新整个页面。
1. webpack-dev-server
使用 webpack 内置的 HMR 插件,更新 webpack-dev-server 配置。
module.export = {
devServer: {
contentBase: './dist',
hot: true, // 热更新
},
}
2. vue-hot-reload-api
Vue热更新插件
3. Q&A
Q:配置了 SpeedMeasurePlugin 后,热更新就无效了,会提示 runtime is undefined
A:仅在分析构建速度时打开 SpeedMeasurePlugin 插件,这里先关闭 SpeedMeasurePlugin 的使用,来查看热更新效果
三、加快构建速度
1. 更新版本
1.1. webpac4 -> 5
webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化
1.2. 包管理工具
将 Node.js 、package 管理工具(例如 npm 或者 yarn)更新到最新版本,也有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。
yarn在生产环境下,自动编译、部署等流程自动化、持续集成过程中,更加安全的保证 package.json & package-lock.json 中依赖包的版本一致性。
较新版本:
webpack@5.46.0node@14.15.0npm@6.14.8
2. 缓存
2.1 cache
通过配置 webpack 持久化缓存 cache: filesystem,来缓存生成的 webpack 模块和 chunk,改善构建速度。
简单来说,通过 cache: filesystem 可以将构建过程的 webpack 模板进行缓存,大幅提升二次构建速度、打包速度,当构建突然中断,二次进行构建时,可以直接从缓存中拉取。
webpack.common.js 配置方式如下:
module.exports = {
cache: {
type: 'filesystem', // 使用文件缓存
},
}
引入缓存后,首次构建时间将增加 15%,二次构建时间将减少 90%
2.2 DLL ❌
在 webpack 官网构建性能 中看到关于 dll 的介绍:
使用
DllPlugin为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。
dll 的相关配置相对复杂。在 github 找到 autodll-webpack-plugin 辅助配置 dll 的插件,结果上面直接写了
webpack5 开箱即用的持久缓存是比 dll 更优的解决方案。
SO,不再配置 dll,cache 真香。
2.3 cache-loader ❌
cache-loader 同样不需要引入,webpack5 的 cache 已经实现了缓存。
3. 减少loader、plugin
每个的 loader、plugin 都有其启动时间。尽量少地使用工具,将非必须的 loader、plugins 删除。
3.1 指定include
webpack构建性能文档 为 loader 指定 include,减少 loader 应用范围,仅应用于最少数量的必要模块。
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
3.2 管理资源
使用 webpack 资源模块 asset module 代替旧的 assets loader(如 file-loader/url-loader/raw-loader 等),减少 loader 配置数量。
4. 优化resolve(解析模块) 配置
resolve 用来配置 webpack 如何解析模块,可通过优化 resolve 配置来覆盖默认配置项,减少解析范围。
4.1 alias
4.2 extensions
4.3 modules
4.4 symlinks
5. 多进程构建
假如某个 loader 的构建时间占据了整个构建过程的 50% 以上,那么有没有方法来加快 loader 的构建速度呢?
可以通过多进程来实现,试想将 loader 放在一个独立的 worker 池中运行,就不会阻碍其他 loader 的构建了,可以大大加快构建速度。
5.1 thread-loader
通过 thread-loader 将耗时的 loader 放在一个独立的 worker 池中运行,加快 loader 构建速度。
安装:
npm i -D thread-loader
webpack.common.js 配置:
module.exports = {
rules: [{
test: /.module.(scss|sass)$/,
include: path.resolve('src'),
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
],
],
},
},
},
{
loader: 'thread-loader',
options: {
workerParallelJobs: 2
}
},
'sass-loader',
],
}]
}
webpack 官网 提到
node-sass中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用thread-loader时,需要设置workerParallelJobs: 2。
由于 thread-loader 引入后,每个 worker 都是一个独立的 node.js 进程,需要 0.6s 左右的时间的开销。 因此,我们应该仅在非常耗时的 loader 前引入 thread-loader。
5.2 happypack ❌
happypack 同样是用来设置多线程,但在 webpack5 中就不要再使用 happypack 了,官方也已经不再维护,推荐使用上文的 thread-loader。
6. 区分 production/development 环境
在开发过程中,切忌在开发环境使用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash] 等工具。
同样,在生产环境,也应避免使用开发环境才会用到的工具,如 webpack-dev-server 等插件。
7. SourceMap 路径信息
四、减小打包体积
1. 代码压缩(js & css)
体积优化第一步是压缩代码,通过 webpack 插件,将 JS、CSS 等文件进行压缩。
1.1 JS压缩
使用 TerserWebpackPlugin 来压缩 JavaScript。
webpack5 自带最新的 terser-webpack-plugin,无需手动安装。
terser-webpack-plugin 默认开启了 parallel: true 配置,并发运行的默认数量: os.cpus().length - 1 ,本文配置的 parallel 数量为 4,使用多进程并发运行压缩以提高构建速度。
webpack.prod.js 配置:
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
}),
]
}
}
ParallelUglifyPlugin ❌
你可能有听过 ParallelUglifyPlugin 插件,它可以帮助我们多进程压缩 JS,webpack5 的 TerserWebpackPlugin 默认就开启了多进程和缓存,无需再引入 ParallelUglifyPlugin。
1.2 CSS压缩
使用 CssMinimizerWebpackPlugin 压缩 CSS 文件。
和 optimize-css-assets-webpack-plugin 相比,css-minimizer-webpack-plugin 在 source maps 和 assets 中使用查询字符串会更加准确,而且支持缓存和并发模式下运行。
CssMinimizerWebpackPlugin 将在 Webpack 构建期间搜索 CSS 文件,优化、压缩 CSS。
安装:
npm install -D css-minimizer-webpack-plugin
webpack.prod.js 配置:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin({
parallel: 4,
}),
],
}
}
由于 CSS 默认是放在 JS 文件中,因此本示例是基于下章节将 CSS 代码分离后的效果。
2. 代码分离(模块&css)
代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间。
2.1 抽离重复代码
SplitChunksPlugin 插件开箱即用,可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
通过 splitChunks 把 vue 等公共库抽离出来,不重复引入占用体积。
注意:切记不要为 cacheGroups 定义固定的 name,因为 cacheGroups.name 指定字符串或始终返回相同字符串的函数时,会将所有常见模块和 vendor 合并为一个 chunk。这会导致更大的初始下载量并减慢页面加载速度。
webpack.prod.js 配置:
module.exports = {
splitChunks: {
// include all types of chunks
chunks: 'all',
// 重复打包问题
cacheGroups:{
vendors:{ // node_modules里的代码
test: /[\/]node_modules[\/]/,
chunks: "all",
// name: 'vendors', 一定不要定义固定的name
priority: 10, // 优先级
enforce: true
}
}
},
}
2.2 CSS 文件分离
MiniCssExtractPlugin 插件将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
安装:
npm install -D mini-css-extract-plugin
webpack.common.js 配置:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /.module.(scss|sass)$/,
include: paths.appSrc,
use: [
'style-loader',
isEnvProduction && MiniCssExtractPlugin.loader, // 仅生产环境
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
],
],
},
},
},
{
loader: 'thread-loader',
options: {
workerParallelJobs: 2
}
},
'sass-loader',
].filter(Boolean),
},
]
},
};
注意:MiniCssExtractPlugin.loader 要放在 style-loader 后面。
2.3 最小化 entry chunk
通过配置 optimization.runtimeChunk = true,为运行时代码创建一个额外的 chunk,减少 entry chunk 体积,提高性能。
webpack.prod.js 配置:
module.exports = {
optimization: {
runtimeChunk: true,
},
};
}
3. Tree Shaking 摇树
顾名思义,就是将枯黄的落叶摇下来,只留下树上活的叶子。枯黄的落叶代表项目中未引用的无用代码,活的树叶代表项目中实际用到的源码。
3.1 JS
JS Tree Shaking 将 JavaScript 上下文中的未引用代码(Dead Code)移除,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供b标识,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
webpack5 sideEffects
通过 package.json 的 "sideEffects" 属性,来实现这种方式。
对组件库引用的优化
webpack5 sideEffects 只能清除无副作用的引用,而有副作用的引用则只能通过优化引用方式来进行 Tree Shaking。
-
lodash
类似
import { throttle } from 'lodash'就属于有副作用的引用,会将整个 lodash 文件进行打包。优化方式是使用
import { throttle } from 'lodash-es'代替import { throttle } from 'lodash',lodash-es 将 Lodash 库导出为 ES 模块,支持基于 ES modules 的 tree shaking,实现按需引入。 -
ant-design
ant-design 默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入
import { Button } from 'antd'就会有按需加载的效果。假如项目中仅引入少部分组件,
import { Button } from 'antd'也属于有副作用,webpack不能把其他组件进行tree-shaking。这时可以缩小引用范围,将引入方式修改为import { Button } from 'antd/lib/button'来进一步优化。
3.2 CSS
上述对 JS 代码做了 Tree Shaking 操作,同样,CSS 代码也需要摇摇树,打包时把没有用的 CSS 代码摇走,可以大幅减少打包后的 CSS 文件大小。
使用 purgecss-webpack-plugin 对 CSS Tree Shaking。
安装:
npm i purgecss-webpack-plugin -D
因为打包时 CSS 默认放在 JS 文件内,因此要结合 webpack 分离 CSS 文件插件 mini-css-extract-plugin 一起使用,先将 CSS 文件分离,再进行 CSS Tree Shaking。
webpack.prod.js 配置方式如下:
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const paths = require('paths')
module.exports = {
plugins: [
// 打包体积分析
new BundleAnalyzerPlugin(),
// 提取 CSS
new MiniCssExtractPlugin({
filename: "[name].css",
}),
// CSS Tree Shaking
new PurgeCSSPlugin({
paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }),
}),
]
}
4. CDN(手动上传字体、图片至网络节点)
上述是对 webpack 配置的优化,另一方面还可以通过 CDN 来减小打包体积。
这里引入 CDN 的首要目的为了减少打包体积,因此仅仅将一部分大的静态资源手动上传至 CDN,并修改本地引入路径。
将大的静态资源上传至 CDN:
- 字体:压缩并上传至 CDN;
- 图片:压缩并上传至 CDN。
五、加快加载速度
1. 按需加载
通过 webpack 提供的 import() 语法 动态导入 功能进行代码分离,通过按需加载,大大提升网页加载速度。
因为现在使用 vue-cli3 脚手架创建的vue工程是使用 babel7 编译的,
babel-plugin-component已经不再适用于 babel7,文本将会使用babel-plugin-import实现按需引入的功能。其实babel-plugin-import与babel-plugin-component差异不大,只是做一下语法转换。
2. 浏览器缓存
浏览器缓存,就是进入某个网站后,加载的静态资源被浏览器缓存,再次进入该网站后,将直接拉取缓存资源,加快加载速度。
webpack 支持根据资源内容,创建 hash id,当资源内容发生变化时,将会创建新的 hash id。
配置 JS bundle hash,webpack.common.js 配置方式如下:
module.exports = {
// 输出
output: {
// 仅在生产环境添加 hash
filename: ctx.isEnvProduction ? '[name].[contenthash].bundle.js' : '[name].bundle.js',
},
}
配置 CSS bundle hash,webpack.prod.js 配置方式如下:
module.exports = {
plugins: [
// 提取 CSS
new MiniCssExtractPlugin({
filename: "[hash].[name].css",
}),
],
}
配置 optimization.moduleIds,让公共包 splitChunks 的 hash 不因为新的依赖而改变,减少非必要的 hash 变动
webpack.prod.js 配置:
module.exports = {
optimization: {
moduleIds: 'deterministic',
}
}
通过配置 contenthash/hash,浏览器缓存了未改动的文件,仅重新加载有改动的文件,大大加快加载速度。
3. CDN 缓存
将所有的静态资源,上传至 CDN,通过 CDN 加速来提升加载速度。
webpack.common.js 配置:
export.modules = {
output: {
publicPath: ctx.isEnvProduction ? 'https://xxx.com' : '', // CDN 域名
},
}