前端开发是一个快速发展的领域,每天都会涌现出新的技术和工具。在实现功能的同时,开发人员面临着许多挑战如代码可维护性,加载时间,访问速度,构建速度等问题。这些挑战可能直接影响网站的性能和用户体验,需要采取一些优化措施来改善问题。在本文中,我们将探讨一些前端项目优化的具体措施,旨在帮助开发人员提高项目效率,升级用户体验。
综合各方面来说 就是小, 少, 快
小:减少项目体积
- 使用模块化开发,禁止使用全局变量和函数,尽量减少污染全局变量;
- 合理使用 Tree Shaking 和 Dead Code Elimination,只打包使用到的代码;
- 对于静态资源,尽量使用 CDN 加载外部资源,减轻服务器负担;
- 使用 Code splitting 减小入口文件的大小,按需加载组件和路由;
- 对于需要在不同页面中间切换的模块可以考虑使用 Vue 的 keep-alive 缓存机制。
少:减少 HTTP 请求次数
- 首先明确必须要的请求,其余的请求可以通过 Vue 的异步组件按需加载;
- 将常用的图片、Logo、Icon 等打包到 CSS 文件中,减小图片请求次数;
- 对于 AJAX 请求,可以采用缓存和数据请求合并等优化方式,减少请求次数。
快:提高页面加载速度
- 对于 Vue 项目,使用 Vue-Router 实现路由懒加载、动态加载,以及异步组件等方式;
- 对于静态资源,如图片、CSS、JS 等文件,使用 Gzip 或者 Brotli 等方式进行压缩,减小文件大小;
- 合理使用缓存,如 CDN 缓存、浏览器缓存等;
- 对于 Vue 页面,采用服务端渲染(SSR)可以提高首屏渲染速度;
- 对于代码压缩、合并等优化,现代构建具如 Vue CLI 已经集成了很好的优化策略;
量化工具
在进行项目优化的时候,不能盲目的从网上或者其他文档上找优化配置进行添加,你需要对项目的实际情况进行一个了解, 才能对症下药, 比如你需要知道打包耗了多长时间,具体哪个loderl或者plugin耗时长,打包资源各个模块的体积等等.
所以在开始优化之前,需要安装一些插件,帮助我们分析
progress-bar-webpack-plugin 查看编译进度
// 安装
npm i -D progress-bar-webpack-plugin
// 使用
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)`
})
],
}
// 包含内容、进度条、进度百分比、消耗时间,进度条
speed-measure-webpack-plugin 编译速度分析
这个插件可以我们知道哪些plugin,哪些loader所耗时长,以及总打包编译时长, 帮助我们针对性的优化
// 安装
npm i -D speed-measure-webpack-plugin
// 配置方式一, 如果你是webpack搭建的项目,
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ...webpack config...
})
// 如果你是vuecli搭建的项目, 则如下
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin')
module.exports = {
chainWebpack: config => {
config.plugin('speed')
.use(SpeedMeasureWebpackPlugin)
}
}
效果如下
webpack-bundle-analyzer 打包体积优化
这个插件可以让我们看到打包后的资源之中每个模块的大小``
// 安装
npm i -D webpack-bundle-analyzer
// 使用 如果是webpack搭建的项目
// webpack.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
// 打包体积分析
new BundleAnalyzerPlugin()
],
}
**// 如果是vuecli 则无需安装 已经内部安装了**
直接使用 npm run build --report 命令 或者 npm run build -- --report
就会在打包目录下生成一个report.html文件,打开这个文件就知道了
配置指南
任何优化,都要对症下药,而不是盲目的从网上找配置一股脑地堆上去, 比如说启动慢,构建慢,则可以用下面提到的缓存和多核多进程相关的配置进行提升, 优化打包资源则需要要具体分析,如下图,这个是一个项目里面的打包资源分析(通过webpack-bundle-analyzer
生成),可以清晰的知道生成打包资源各个模块的大小,进而针对性的优化.
打包生成的资源大小
开启压缩后,gizp资源的大小
使用缓存能力
增加对应的缓存配置, 可以很好的提升构建和启动速度, 尤其是多次构建和启动(或热更新)的场景
cache-loader 与cache 配置
在webpack搭建的项目中,可以使用上述两个东西,改善构建速度
但是在vuecli创建的项目则不用
VueCli
自带
- cache-loader 会默认为
Vue/Babel/TypeScript
编译开启。文件会缓存在node_modules/.cache
中。 如果你遇到了编译方面的问题,记得先清缓存目录之后再试试看。 - thread-loader 会在多核 CPU 的机器上为
Babel/TypeScript
转译开启。
hard-source-webpack-plugin
这个插件主要是利用缓存,提升二次启动和二次打包的速度, 效果很直观
使用方式
// vue.config.js 在vuecli中
const HardSourcePlugin = require('hard-source-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [new HardSourcePlugin()]
}
}
// webpack.config.js 在webpack搭建的项目中
const HardSourcePlugin = require('hard-source-webpack-plugin')
module.exports = {
mode: 'development',
...
plugins: [new HardSourcePlugin()]
}
开启babel缓存
只需要在babel-loader
的配置里加一个参数即可:cacheDirectory: true
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
},
生成gizp的打包资源
打包的时候开启gzip可以很大程度减少包的大小,页面大小可以变为原来的30%甚至更小,非常适合线上部署, 但还记得需要服务端支持
compression-webpack-plugin
前端配置
]// vue.config.js 在vuecli中
/* 第一种方式 */
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const plugins = [...]
if (isProduction) {
plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\.(' + ['html', 'js', 'css'].join('|') + ')$'),
threshold: 10240, // 只有大小大于该值的资源会被处理 10240
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
})
)
}
module.exports = {
configureWebpack: {
resolve: {
alias: {
'@': resolve('./src')
}
},
plugins
},
}
/* 第二种方式 在vuecli中*/
// configureWebpack 返回函数式写法
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const isProduction = process.env.NODE_ENV !== 'development'
module.exports = {
...
configureWebpack: (config) => {
config.plugins.push(
new HardSourcePlugin()
)
// 其他配置
Object.assign(config.resolve, {
alias: {
'@': resolve('./src')
}
})
// 为生产环境修改配置
if (isProduction) {
config.plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\.(' + ['html', 'js', 'css'].join('|') + ')$'),
threshold: 10240, // 只有大小大于该值的资源会被处理 10240
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
})
)
}
},
}
// webpack.config.js 在webpack搭建的项目中
const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = {
mode: 'development',
...
plugins: [
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\.(' + ['html', 'js', 'css'].join('|') + ')$'),
threshold: 10240, // 只有大小大于该值的资源会被处理 10240
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
})
]
服务端配置, 以nginx为例
服务端 Nginx 需开启 gzip_static 功能;
在nginx.conf的server模块中加入以下代码:
server{
//开启和关闭gzip模式
gzip on;
//gizp压缩起点,文件大于2k才进行压缩;设置允许压缩的页面最小字节数,页面字节数从header头得content-length中进行获取。 默认值是0,不管页面多大都压缩。建议设置成大于2k的字节数,小于2k可能会越压越大。
gzip_min_length 2k;
// 设置压缩所需要的缓冲区大小,以4k为单位,如果文件为7k则申请2*4k的缓冲区
gzip_buffers 4 16k;
// 设置gzip压缩针对的HTTP协议版本
gzip_http_version 1.0;
// gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_comp_level 2;
//进行压缩的文件类型
gzip_types text/plain application/javascript text/css application/xml;
// 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
}
访问时 可以看到
利用多核/多进程能力
thread-loader
- 多进程程处理loader
- 使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。
- 本身启动进程就需要耗费时间,**请仅在耗时的操作中使用此 loader!**要不然收益达不到你的预期
happypack
- 启动多进程来优化构建速度,但是作者不维护了,注意安装版本,
- 替换方案建议使用
thread-loader
{
// 当js代码用babel处理兼容性且本身代码就比较多的情况下可以使用thread-loader开启多线程打包
// thread-loader本身启动进程就需要耗费时间, 所以当js代码不多时就不要开启, 否则还可能会延长打包时间
test: '/.js$/',
exclude: /node_modules/,
use: [
{
loader: 'thread-loader',
options: {
// 默认是cpu核 - 1, 可以通过这里更改开启几个进程
works: 2
}
}
]
}
// @file: webpack.config.js
const HappyPack = require('happypack');
var happyThreadPool = HappyPack.ThreadPool({ size: 5 });
exports.module = {
rules: [
{
test: /.js$/,
// 1) replace your original list of loaders with "happypack/loader":
// loaders: [ 'babel-loader?presets[]=es2015' ],
use: ['happypack/loader'?id=babel], // 这里的id 就是定义在plugin里面HappyPack实例构造参数传入的id
include: [ /* ... */ ],
exclude: [ /* ... */ ]
},
{
test: /.less$/,
use: 'happypack/loader?id=styles'
},
]
};
exports.plugins = [
// 2) create the plugin:
new HappyPack({
// 3) re-add the loaders you replaced above in #1:
loaders: [ 'babel-loader?presets[]=es2015' ],
threadPool: happyThreadPool,
id: 'babel'
}),
new HappyPack({
id: 'styles',
threadPool: happyThreadPool,
loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
})
];
注意: 这部分,如果你是vueCli创建的项目,则不用配置, 内部已处理
关闭source map
生产环境关闭productionSourceMap
、css sourceMap
, 生产环境没必要打开这个
// vue.config.js
const isProduction = process.env.NODE_ENV === 'production'
// 判断是否是生产环境
module.exports = {
productionSourceMap: !isProduction, //关闭生产环境下的SourceMap映射文件
css: {
sourceMap: !isProduction, // css sourceMap 配置
loaderOptions: {
...其它代码
}
},
...其它代码
}
组件的按需加载配置
这里以ElementUi 为例, 其他组件库官网上会有对应说明, 大体上差不多
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
npm install babel-plugin-component -D
// .babelrc 或者 babel.config.js
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
如图,可以看到对比,
还有需要注意的是,如果你使用了自定义主题, 那么在babel的配置中如官网以下
解决Echarts打包后体积过大问题
从刚开始的图中,能看到, echarts占了很大的体积, 这不能接受
方案一
在index.html模板文件中引入cdn, 打包的时候webpack配置externals ,这样webpack就不会打这个包到vendors里,减少体积,缺点是cnd稳定性可能会有问题,而且是先请求cdn资源之后再请求自己服务器代码,非同源。pass
方案二
babel-plugin-equire
我见社区有这个按需引入echarts的插件, 不过我没研究过, 感兴趣的可以去试试,我选择的是方案三
方案三
在线定制echarts, 可以选择自己想要的功能,生成的js,就会小很多,
在线定制网址
将生成的echarts.min.js引入到项目中,替换项目中的echarts引用
import echarts from 'echarts'
变成下面
import echarts from './plugins/echarts.min.js'
也可将这个js放到index.html里面,
<script src="./static/lib/echarts.min.js"></script>
在main.js文件里面
Vue.prototype.$echarts = window.echarts
同时可以卸载调之前装的echarts
npm unistall echarts
可以看到 还是很明显的变化的
接下来 再看看打包生成的report现状
可以看到pdf占比还是很大, 继续优化
pdf插件优化
项目中有pdf预览的功能,安装了pdfjs-dist
和vue-pdf
两个插件,后来跟后端协商,走文件流的方式了, 就没用到这两个, 就卸载了, 如果谁有相关方案,欢迎来提
moment.js的优化
如果时间充裕,建议换成day.js 大小只有6kb, 但是我们项目里面用moment地方太多了,且比较混乱, 就暂时先不更换, 采用剔除其他语言包的方案
方案一
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
module.exports = {
configureWebpack: {
plugins: [
new MomentLocalesPlugin({
// 保留一个
localesToKeep: ['zh-cn'],
}),
]
}
}
//使用
import moment from 'moment';
// 按需加载需要引入对应的语言包
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
方案二
const webpack = require('webpack');
module.exports = {
//...
plugins: [
// 忽略 moment.js的所有本地文件
new webpack.IgnorePlugin(/^./locale$/, /moment$/),
],
};
// 使用
import moment from 'moment';
// 按需加载需要引入对应的语言包
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
针对lodash库优化
项目中有时会安装lodash,但一般只使用几个方法, 在打包的时候,会发现会将lodash全部打进去, 所以使用下面这两个库
babel-plugin-lodash
用来精简Lodash模块的,只保留用到的方法。lodash-webpack-plugin
这个插件经过用noop, identity, 或其余更简单的替代品来替换一些模块的特性,使得打包后的体积更小(翻译)。
注意:这个插件默认会关闭一些lodash不经常使用的特性,能够给插件传递options来开启某些特性
modules.exports = {
// 其余配置省略...
plugins: ['lodash']
}
// vue.config.js
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
module.exports = {
// ...
configureWebpack: {
plugins: [
new LodashModuleReplacementPlugin()
]
}
}
// 或者chainWebpack 但是我觉得这种写法,有点一言难尽
module.exports = {
chainWebpack: config => {
config.plugin("loadshReplace")
.use(new LodashModuleReplacementPlugin());
}
}
注意: 这个插件可能带来一些意想不到的坑, 可能会导致使用第三方插件报错出现一些其他问题,具体可以参考下这篇文章 zhuanlan.zhihu.com/p/349260482
开启 Tree Shaking 功能
在前面的依赖视图分析中可以看到sailfish 这个依赖,如下图,它是我们公司以前封装的UI库,它是以源码的方式供项目使用, 没有打包. 老项目里面用了几个组件,打包的时候发现还是全部打进来了
优化方式有以下几个点
- 按需加载:在需要使用这个 UI 库的组件时再进行动态加载,不在打包时将整个库全部打入。
- 开启 Tree Shaking:如果你使用的是 ES6 模块化的方式引入这个 UI 库,并且使用了相关的工具(如 webpack),则可以开启 Tree Shaking 功能,只将你用到的组件打入打包文件中,未使用的组件将会被去除。
- 使用 babel-plugin-import 插件:有些 UI 库将所有的组件都导出为一个模块,对于这种情况,你可以使用 babel-plugin-import 插件,通过按需加载的方式只加载你使用的组件,而不是全部导入。
- 自己打包:如果这个 UI 库的源码是开放的,而且你只想使用部分组件,那么你也可以自己将需要的组件单独打包,而不是全部打包。
因为后期规划要替换掉它,所以不想在它上面浪费过多精力
其他的优化
externals
externals
配置项用来告诉Webpack
要构建的代码中使用了哪些不用被打包的模块,也就是说这些模版是外部环境提供的,Webpack
在打包时可以忽略它们
一般是将体积较大的第三方包抽离为externals,一般处理element-ui
,vue
或者其他的,看自己项目
// vue.config.js
module.exports = {
configureWebpack: {
externals: {
// 安装的包名 --- 暴露的全局变量的值
"element-ui": "ELEMENT",
echarts: "echarts",
vue: "Vue",
},
},
}
然后在index.html引入对应的cnd链接, 如果害怕cdn不稳定,可以去下载下来,copy到自己的项目中,手动引入, 也可使用webpack配置注入进去
// vue.config.js
module.exports = {
chainWebpack: config => {
config.plugin('html').tap(args => {
args[0].cdn = {
js: [
'https://xx.com/CDN/js/[email protected]',
],
css: [
'https://xx.com/CDN/css/element-ui2.13.0/index.css',
],
};
return args;
});
}
}
// 使用
<!DOCTYPE html>
<html lang="zh">
<head>
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style">
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
<% } %>
<!-- 使用 CDN 加速的 JS 文件,配置在 vue.config.js 下 -->
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>" type="text/javascript"></script>
<% } %>
</head>
<body>
<div id="app"></div>
</body>
</html>
</html>
注意
- externals:声明文件被外部引用不用打包,不参与打包流程,由于直接写死在html里,可以使用cdn等加速
- externals缺点:直接html内引入的,所以不会有treeshaking,按需引入等,具体看自己取舍
推荐常用的两个cdn服务 BootCDN 与 cdnjs 。
移除 preload(预载) 与 prefetch (预取)
vue 脚手架默认开启了 preload 与 prefetch,当我们项目很大时,会造成首屏加载速度慢的元凶
preload 与 prefetch 都是一种资源预加载机制; preload 是预先加载资源,但并不执行,只有需要时才执行它; prefetch 是意图预获取一些资源,以备下一个导航/页面使用; preload 的优先级高于 prefetch。
// vue.config.js
chainWebpack: config => {
// 移除 preload(预载) 插件
config.plugins.delete('preload')
// 移除 prefetch(预取) 插件
config.plugins.delete('prefetch')
}
主要关注首屏速度, 可自行去体验下关闭前后的效果
清除log等调试信息
使用 terser-webpack-plugin 清除 console.log
先安装依赖
npm install terser-webpack-plugin --save-dev
然后配置
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
configureWebpack: config => {
// 生产环境下清除 console.log
if (process.env.NODE_ENV === 'production') {
return {
optimization: {
minimizer: [
new TerserPlugin({
sourceMap: false,
terserOptions: {
compress: {
drop_console: true
}
}
})
]
}
}
}
}
}
分包拆包
一般拆包原则如下
- 将变动的与不易变动的资源进行分离 —> 有效利用缓存;* 将 node_modules 中的资源拆分出来,如果 node_modules 中的资源不变,就可以有效利用缓存,避免受到业务代码频繁改动的影响;
- 将大的拆分成若干个小的 chunk —> 缩短单个资源下载时间;
- 将公共模块抽离出来 —> 避免资源被重复打包,这样也可以在一定程度上减小打包产物总体积;
- 将被多个 chunk 引用的包拆分成单独的模块;
要根据项目情况灵活配置,我就贴一个我在项目里使用的
// vue.config.js
module.exports = {
chainWebpack(config) {
config.when(process.env.NODE_ENV !== 'development',config => {
config
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\/]node_modules[\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\/]node_modules[\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
config.optimization.runtimeChunk('single')
}
)
}
}
压缩图片
项目里面后期肯定会放不少图片进去, 这些图片如果不压缩以下,占的资源就会非常大,社区里面有不少插件可以进行压缩,但我个人还是很喜欢熊猫压缩
推荐大家使用. 可以整个文件夹丢进去
结束语
一般常用的就这些,当然还有很多其他的优化方案, 这里没有绝对的最佳配置,只有最佳实践,大家根据自己的项目实际情况,逐步分析优化.