为什么要升级
我们 App 框架是基于vue2+ webpack3 支持国际化的多页面应用,比较通用。文件目录结构大概如下
project/
├── README.md
├── package.json
├── babel.config.js // Babel配置
├── build/ // webpack配置相关
├── static/ // 静态资源目录
├── src/
│ ├── assets/ // 静态资源文件夹(图片、字体等)
│ ├── components/ // 共享组件
│ ├── pages/ // 多个页面或路由入口
│ │ ├── user/
│ │ │ ├── locales/ // 用户模块相关的国际化语言包
│ │ │ │ ├── en/ // 英语语言包
│ │ │ │ │ └── messages.js
│ │ │ │ ├── zh-CN/ // 简体中文语言包
│ │ │ │ │ └── messages.js
│ │ │ │ └── ... // 其他语言包
│ │ │ ├── login/
│ │ │ │ ├── index.vue // 登录页面主组件
│ │ │ │ ├── main.js // 登录页面入口js
│ │ │ │ └── index.html // (可选)登录页面入口
│ │ │ ├── register/
│ │ │ │ ├── index.vue // 注册页面主组件
│ │ │ │ ├── main.js // 注册页面入口js
│ │ │ │ └── index.html // (可选)注册页面入口
│ │ ├── page2/
│ │ └── ...
│ └── utils/ // 工具函数及公用模块
└── .gitignore
框架使用两年后,发现可以从以下方向进一步优化:
-
框架不支持新的 ES 语法,比如
?.。初步研究方案:需要升级到 babel7,包括它所依赖的 npm 包,那么影响的页面是全量的,全量回归! -
框架构建速度慢。页面数量80个左右,本地服务启动时间约 118 秒,生产构建时间 205 秒。我们另一个项目也是基于此框架,页面数量200个左右,本地服务慢到无法启动。
-
页面的公共样式和 js 并没有提取,生产构建包大小 20MB。如果能提取公共文件,比如各页面公用的 vue、vue-i18n 和 lodash 等库可以合并为一个 js 公用。这样构建包会更小,运行时各页面可以共用缓存,同时减少页面首屏渲染时间。
-
页面部分静态资源HTTP缓存设置不正确。会导致发版本后文件有更新,用户访问命中到旧的文件。
- 有些静态资源比如 style.css,是通引入的,并没有交给 webpack 管理。生产构建后如下
-
<link rel="stylesheet" href="style.css"> - 静态资源响应头HTTP缓存设置如下,也就是强制缓存2小时。
-
Cache-Control:max-age=7200
-
使用
webpack-bundle-analyzer分析构建结果:有冗余的js引入。 -
使用
Chrome lighthouse分析,建议去掉冗余js和css,优化静态资源加载顺序 -
static目录下的文件是全部复制到构建目录的,包含了大量无用的文件 -
i18n文件多个页面共用,打包进每个页面,增加了包大小。
i18n文件放在页面级别
pages/user/login和pages/user/register两个页面用i18n文件是这样写的
module.exports = {
back: '返回',
login: {
title: '登录'
...
},
register: {
title: '注册'
...
}
}
这种目录结构的优点:
- login和register页面都共用一些国际化Key,比如"back"
- 共用的国际化Key,一处修改,全局生效
缺点也很明显
-
很难评估某个国际化Key是否是共用的。勉强用了,需要更新的时候,由于难以评估影响范围,只好在页面再声明一份。
- 比如back的值需要改为"取消",影响到的页面都要评估下
-
构建包包含的国际化文件冗余。比如login页面包括了语言文件中的register对象。
结论:国际化文件放在页面级别
各页面有独立的locales, 调整目录结构如下:
src/
── pages/
├── user/
│ ├── login/
│ │ ├── locales/ // 登录页面相关的国际化语言包
│ │ │ ├── en/ // 英语语言包
│ │ │ ├── zh-CN/ // 简体中文语言包
│ │ │ └── ... // 其他语言包
│ │ ├── index.vue // 登录页面主组件
│ │ ├── main.js // 登录页面入口js
│ │ └── (index.html) // (可选)登录页面入口HTML
│ ├── register/
│ │ ├── locales/ // 注册页面相关的国际化语言包
│ │ │ ├── en/ // 英语语言包
│ │ │ ├── zh-CN/ // 简体中文语言包
│ │ │ └── ... // 其他语言包
│ │ ├── index.vue // 注册页面主组件
│ │ ├── main.js // 注册页面入口js
│ │ └── (index.html) // (可选)注册页面入口HTML
└── ... // 其他页面或功能模块
login和register有自己独立的locales,优点如下
-
修改locales不会影响其他页面
-
构建包只包括当前页面下的locales文件
总结
i18n文件放在页面级别,项目构建包大小平均减少30%。
框架升级为Vue-cli
当前框架基于webpack3, 而webpack3 发布于2017年,最新的webpack版本是5。Vue-cli集成了webpack的能力,配置比webpack简单,首先想到的是框架升级为Vue-cli5。
升级步骤
-
node版本升级16+。因为vue-cli依赖。
-
配置文件
vue-config.js的主要配置说明:- 页面多入口pages
- 提取各页面公共库:vue和vue-i18n为公共js,取名chunk-libs.js
- 其他逻辑和原框架webpack配置基本一致。
// 。。。省略处理多页面入口pages
module.exports = defineConfig({
// ...
pages, // 多页面入口
configureWebpack: config => {
config.resolve = {
// ...
};
config.plugins.push(
new webpack.DefinePlugin({
// ...
})
);
if (process.env.NODE_ENV === 'production') {
config.plugins.push(
// 复制static目录
new CopyWebpackPlugin({
pattern:[{ from: 'static', to: 'static'}]
})
);
config.plugins.push({
apply: compiler => {
// 构建完成后,处理逻辑
compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => {
})
}
})
}
},
chainWebpack (config) {
// ...
// 提取各页面公共vue和vue-i18n为公共js, 取名为chunk-libs.js
config.optimization.splitChunks({
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\/]node_modules[\/](vue|vue-i18n)/,
priority: 10,
chunks: 'initial'
}
}
})
}
})
升级效果
生产构建速度提升约4倍。本地服务启动时间107秒,生产构建时间56秒,包大小17.3MB。
框架升级为Webpack5
为什么要改Vue-cli为webpack5?
- 需要修改构建配置提升页面性能,Vue-cli的可定制性不如webpack, 比如: 提取css,压缩css, 去冗余css, 提取公共js等。
- Vue-cli相关资料没有webpack丰富
- webpack5更流行更通用,易于交流
- Vue-cli封装了webpack,webpack更接近原理
升级步骤
-
项目结构
- 样式文件挪到
src/assets/css目录。由页面入口文件main.js引入,这样webpack loader会对样式文件压缩、去冗余处理、提取公共样式文件、生成带hash的文件
import '@/assets/css/style.css' ``` 1. 图片挪到`src/assets/images`目录。这样webpack会压缩图片,并生成带hash的图片 ```html <template> <div> <img :src="imgUrl"/> </div> </tempalte> <script> data (){ imgUrl: require('@/assets/images/banner.png') } </script> ``` - 样式文件挪到
-
开发环境webpack.dev.config.js的主要配置说明:
- babal-loader配置
@babel-preset-env。 后者会根据browserslist,对目标环境作JS语法兼容。这样我们就不用把完整的polyfill全部引入到最终文件里,可以大大减少体积 - eslint使用eslint-webpack-plugin
// 获取页面js入口和html入口, 初始化HTMLWebpackPlugin // ... module.exports = { mode: 'development', stats: 'minimal', entry, // 打包入口地址 output: { filename: '[name].js', publicPath: '/' }, // ... module: { rules: [ { test: /.js$/i, use: [ { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } ] } ] }, plugins: [ new EslintWebpackPlugin({ //... }), new CopyWebpackPlugin({ patterns: [{ from: 'static', to: 'static'}] }), // htmlWebpackPlugin包含js和html入口 ...htmlWebpackPluginList, new VueLoaderPlugin(), // ... ] } ``` - babal-loader配置
-
生产环境webpack.prod.config.js,主要配置说明
- 用webpack默认splitChunks方式进行代码分割
- 使用TerserPlugin进行js压缩
- 提取.vue文件中的样式生成css,由splitChunks进行分割
- 提取的js、css、和图片都添加contenthash。文件内容有变化时,contenthash才会变化。这样有利于及时更新缓存,又高效利用缓存。
// 获取页面js入口及html入口,并初始化HTMLWebpackPlugin // ... const webpackConfig = { mode: 'production', stats: 'minimal', entry, //打包入口地址 output: { clean: true, filename: 'static/js/[name].[contenthash:8].js', // 输入js文件名 path: path.join(__dirname, `../dist`) // 输入文件目录 }, devtool: false, resolve: { // ... }, module:{ rules: [ // ... ] }, optimization: { minimize: true, splitChunks:{ // 表示选择哪些chunks进行分割,可选值有async,initial和all chunks: 'all' }, minimizer: [ // 添加css压缩配置 new CssMinimizerPlugin(), new TerserPlugin({ parallel: true, // 启用多进程 exclude: /node_modules/, // 匹配不需要压缩的文件 extractComments: false, //不提取注释 terserOptions: { compress:{ arguments: true, dead_code: true, drop_console: true, drop_debugger: true, }, format:{ comments: false // 删除注释 } } }) ] }, plugins: [ new CopyWebpackPlugin({ patterns: [{ from: 'static', to: 'static'}] }), // htmlWebpackPlugin包含js和html入口 ...htmlWebpackPluginList, // 抽取css样式,放在static/css目录下 new MiniCssExtractPlugin({ filename: 'static/css/[name].[contenthash:9].css' }) new VueLoaderPlugin(), // ... ] } ```
升级效果
本地服务启动速度提升3倍,生产构建时间提升5倍,生产构建包大小减小60%。
本地服务启动时间36秒,生产构建时间44秒,包大小7.1MB。
经验总结
-
升级过程中比较多时间花在解决npm包冲突上
- chalk报错。是因为版本过高,ES6无法解析。使用chalk@4.1.2版本
- 解析vue2的vue-loader版本15.x
-
Webpack cache设置filesystem的确可以大幅非首次的启动速度,但是修改package.json相关的环境变量后,并不生效。故不再使用。
-
purgecss-webpack-plugin用于删除无用css,但实测会莫名删除有用的样式。故不使用。
进一步优化本地服务
- Eslint检查加入缓存设置
- 本地服务启动,babel不必兼容低版本浏览器
- 本地服务启动,postcss-loader样式兼容不需要
new ESLintWebpackPlugin({
// ...
cache: true
})
// package.json文件
"broswerslist": {
"development": [
"chrome>100"
],
"production": [
// ...
]
}
升级效果
本地服务启动时间18秒, 热更新时间3秒
Webpack Analyzer分析优化
此处图略去
- 模块中字符串使用+方式。因为模板字符串中的表达式是动态解析的,tree shaking不生效。
// 模板字符串方式
export const url = `${baseUrl}/app/user/login`
// 字符串+方式
export const url = baseUrl + '/app/user/login'
- 其中一处时间久远的库文件使用lodash, 仅此一处使用lodash。因此简化此方法,删除lodash.js
webpack在生产模式下默认tree shaking所有未标记为副作用的代码。
Chrome LightHouse分析优化
此处图略去
-
ui库js和css阻塞渲染,最好使用模块化方式引入。据可考经验,ui库不支持。
- 这样webpack可以理清依赖,首屏渲染时最小引入。
// html引入
<link src="ui.css">
<script src="ui.js">
// js模块引入
import 'ui.css'
import 'ui.js'
// 保证ui库初始化时有vue.js
UI.use(Vue)
-
其他第三方js,如native jsbridge、事件上报等,均不支持npm模块化
总结
- 框架优化大幅提升了构建速度、减小了包大小。主要原因在于webpack5优秀的性能。
- 首屏渲染进步不大,主要是因为第三方js阻塞沉浸,并不支持npm模块。
webpack5为何如此优秀,通义千问答:
Webpack 5在性能优化方面相较于之前的版本有显著提升,主要体现在以下几个方面:
- 持久化缓存(Persistent Caching): Webpack 5 引入了更强大的持久化缓存机制,默认配置下即可实现更好的缓存效果。这有助于在多次构建之间重用已编译的结果,从而极大地提高开发环境和持续集成/部署环境下的构建速度。
- Tree Shaking 优化: Tree Shaking 是指去除未使用的模块代码以减小包体积的过程。Webpack 5 在这一方面进行了改进,使得对ES模块的静态分析更加精确,能更有效地移除无用代码。
- 代码生成与压缩优化: Webpack 5 改进了其代码生成器和其他相关工具,如引入
css-minimizer-webpack-plugin替代旧版插件,它提供更好的CSS压缩性能。同时,通过与其他新的或更新的加载器配合,可以更高效地处理资源文件,比如图片、字体等。- 内存管理改进: Webpack 5 对内存使用进行了优化,减少内存泄漏的风险,并提高了大型项目中构建时的内存效率。
- API兼容性调整: 清除了之前遗留下来的为了向后兼容而存在的部分API,这些API可能会影响整体性能。去掉这些冗余后,Webpack 能够运行得更为轻量化和高效。
- 模块解析优化: Webpack 5 提高了模块解析的速度,减少了构建过程中的瓶颈。
- 内置性能提示: Webpack 5 内置了一些功能来帮助开发者更好地识别构建过程中的性能问题,例如,提供了更多的性能提示和日志信息。
综上所述,Webpack 5 的性能优秀是由于它在多个关键领域实现了深度优化,包括但不限于构建时间、资源处理效率、内存利用率以及整体架构的精简和现代化。这些改进为开发者带来了更快的构建速度、更小的产出体积以及更流畅的开发体验。