调优成果
优化前 | 优化后 | 优化比例 | |
---|---|---|---|
打包体积 | 32.01 MB | 13.57 MB | 57.61% |
Gzip 体积 | 3.37 MB | 1.47 MB MB | 56.38% |
文件数量 | 82 | 19 | 76.83% |
资源压缩 | 服务器 br 压缩 | 客户端 br 压缩 | 1 |
压缩级别 | 6 | 11(最高) | 1 |
构建速度 | 60s + | 41.1s + | 31.67% |
播放页 FP 耗时 | 明显感知 | 无感知 | 1 |
(以上数据来源为本地测试,仅供参考)
优化目标
- 降低播放页 FP 耗时
- 减少静态资源请求数量
- 提升开发体验
提升构建产物质量
(文章基于 webpack@4.46.0)问题分析
先看项目当前的打包分析结果
以上是用 vue-cli 生成的构建统计报告,它会帮助分析包中包含的模块们的大小,简单列一下报告信息如下:
- 整体打包体积 32.01 MB
- ts.worker.js 是 monaco 编辑器的语言支持文件,主要提供 typescript 语法支持,体积 10.92 MB
- my-details.js 是播放页文件,网站核心资源文件,体积 6.81 MB
- chunk-vendors.js 第三方模块捆绑包,体积 4.14 MB
- html.workar.js 是 monaco 编辑器的 html 语法支持文件
- css.workar.js 是 monaco 编辑器的 css 语法支持文件
- json.worker.js 是 monaco 编辑器的 json 语法支持文件
- app.js 项目入口文件,体积 1.02 MB
- 小文件文件数量太多,加起来接近百个,导致 http 请求过多
经过分析总结,定位问题如下:
- monaco-editor 是最大的问题,体积占据半壁江山,严重影响加载速度,需要优化
- chunk-vendors.js 作为公共模块,构成项目必不可少的一些基础类库,升级频率都不高,但每个页面都需要它们,现在它体积过大,应该在合理范围内拆分成更小一些的 js,以利用浏览器的并发请求,优化首页加载体验。其中包含了三个大家伙:elementUI、moment 和 lodash,更是需要单独做优化
- my-details.js 和 app.js 作为项目主要的资源文件,体积过大,需要优化
- 小文件数量太多,需要合并
解决思路
检查下有没有哪些产出是不必要的,在有限的时间空间和算力下,去除低效的重复(提出公共大模块),进行合理的冗余(小文件允许重复),达到时间和空间综合考量上的最优。
下面分步骤实现每一个优化项。
monaco-editor 优化
缩小体积
我们业务层面,只需要用到展示功能,其语言编辑功能完全用不到,因此可以把语言包全部过滤掉,这里需要用到 monaco-editor-webpack-plugin 插件,配置添加插件选项 languages 为支持的语言数组(具体语言查看官网)默认是支持所有语言的,配置此项应该只是去除一些语言的高级特性支持。
new MonacoWebpackPlugin({
languages: [],
}),
再次打包后,体积缩小至 9.26 MB。
单独打包
monaco-editor 作为一个重量级组件,会分散很多小文件到各个地方,从而增加文件数量和体积,进而造成流量损失,通过 webpack 的 splitChunks 功能拆分 monaco-editor 为单独独立文件,充分利用浏览器缓存,减少多次引用时的消耗。
splitChunks: {
chunks: 'all',
minSize: 20000,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
maxSize: 0,
cacheGroups: {
monacoEditor: {
chunks: 'async',
name: 'chunk-monaco-editor',
priority: 22,
test: /[\/]node_modules[\/]monaco-editor[\/]/,
enforce: true,
reuseExistingChunk: true,
},
}
按需加载
通过按需加载,减少核心内容渲染前的阻塞,转到需要的时候加载(当前仅需要editor.api 和 js css高亮功能)。
async initEditor() {
const monaco = await import(
/* webpackPrefetch: true;" */'monaco-editor/esm/vs/editor/editor.api.js'
);
// 引入高亮模块
await this.highlightLang()
}
// 引入语言高亮模块
highlightLang() {
const LANGUAGES = ['javascript', 'css', 'html', 'mysql', 'java', 'python', 'markdown', 'go', 'lua'];
const promiseList = [
import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'),
import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/css/css.contribution'),
// import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/html/html.contribution'),
// import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/mysql/mysql.contribution'),
// import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/java/java.contribution'),
// import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/python/python.contribution'),
// import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution'),
// import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/go/go.contribution'),
// import(/* webpackPrefetch: true;" */'monaco-editor/esm/vs/basic-languages/lua/lua.contribution'),
];
return Promise.all(promiseList);
}
开启 prefetch
按需加载后,因为 monaco-editor 本身体量很大,因此在加载编辑器时会出现长时间无响应现象,采用 prefetch 预加载方案,在浏览器空闲时预先下载资源,到用的时候直接取,有效避免无响应情况。
const monaco = await import( /* webpackPrefetch: true;" */ 'monaco-editor');
moment.js 优化
尝试删除 moment.js 语言包后体积依然很大,最后采用和 moment.js api 完全兼容的 dayjs 替换,gzip压缩后仅仅 2kb
(因替换工作为全局,可能会出现和 moment.js 相关功能的 bug )
element-ui 优化
单独打包
理论上 UI 组件库也可以放入 chunk-vendors.js 中,但它实在是过大,可能比 libs 里所有的包加起来还要大不少,而且 UI 组件库的更新频率也相对比 chunk-vendors.js 要更高一点。Element-UI 组件库作为 UI 组件,应从 chunk-vendors.js 中分离出来,单独打包为 chunk-elementUI.js,如图所示打包后体积为 1.67 MB
按需引入
按照官方按需引入方式,只引入使用的组件,减少体积。
lodash.js 优化
按需引入
使用 webpack 插件 lodash-webpack-plugin 和 babel 插件实现按需打包 lodash
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
config.plugin('loadshReplace').use(new LodashModuleReplacementPlugin());
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
'lodash',
],
};
优化后,lodash 基本上做到无感知存在
svgIcon优化
单独打包
svgIcon 组件库作为高频更新且引用超多的库,应单独分出为 chunk-svgIcon.js,如图所示打包后体积为 561 kb
删除 use-zh.svg
经过代码审查发现体积最大的 use-zh.svg 在项目中现在并未使用,所以删除
雪碧图
将所有 svg 合成雪碧图,减少请求次数,使用 svg-sprite-loader 实现
const svgRule = config.module.rule('svg');
// 清除已有的所有 loader,如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
svgRule.uses.clear();
// 添加要替换的 loader
svgRule
.test(/.svg$/)
.include.add(path.resolve(__dirname, 'src/components/svgIcon/svg'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]',
})
.end();
压缩 svg (未执行)
因为压缩过后的 svg 图会存在去掉 fill 的情况,因此这一步骤并未执行
代码压缩
采用 terser 插件(webpack4 官方推荐)进行 js 和 css 代码压缩,同时去掉生产环境的注释和 console 信息
config.optimization.minimizer('terser').tap(options => {
const compress = {
warnings: false,
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log'],
};
const initCompress = options[0].terserOptions.compress;
options[0].terserOptions.compress = { ...initCompress, ...compress };
return options;
});
图片压缩(未执行)
使用 webpack 插件 image-webpack-loader 对所有图片进行压缩,但是该插件依赖于系统环境,在不同环境下可能出现安装失败,编译失败等情况,我们项目目前图片资源较少,所以暂时不用。
开启 br 压缩
当前 br 压缩是在 nginx 服务器进行且并未缓存资源,因此每次请求都需要对资源进行压缩之后发出,考虑服务器性能,压缩级别设置为 6。
现在改为客户端压缩,服务器在接收请求时直接把压缩文件发出,减少服务器压力。同时客户端压缩可以把压缩级别调整至最高的 11,整体资源大小会再次下降,使用 compression-webpack-plugin 实现。
const CompressionWebpackPlugin = require('compression-webpack-plugin');
if (!isDev) {
plugins.push(
new CompressionWebpackPlugin({
filename: '[path].br[query]',
algorithm: 'brotliCompress',
test: /.(js|css|json|txt|html|ico|svg)(?.*)?$/i,
compressionOptions: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
},
},
threshold: 1024,
minRatio: 0.99,
//删除原始文件只保留压缩后的文件
deleteOriginalAssets: false,
}),
);
}
多线程执行 loader
开启 parallel 为 Babel 或 TypeScript 使用 thread-loader
parallel: require('os').cpus().length > 1
打包缓存
采用 HardSourceWebpackPlugin 插件为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source。配置 hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。
可能带来的问题,修复办法在这 hard-source-webpack-plugin
下面链接用于解决 hash 丢失问题
https://github.com/mzgoddard/...
其他修改项
- 提取环境变量
const
` isDev = process.env.NODE_ENV === 'development'; ` - 增加打包速度检测插件 SpeedMeasurePlugin
最终报告
报告图:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。