在Vue项目中,引入到工程中的所有js、css文件,编译时都会被打包进vendor.js,浏览器在加载该文件之后才能开始显示首屏。若是引入的库众多,那么vendor.js文件体积将会相当的大,影响首屏的体验。可以看个例子:
这是优化前的页面加载状态:执行 npm run build
打包项目,出来的vendeor.js文件,基本都是1M以上的的巨大文件,没有用户能忍受5s以上的loading而不关闭页面的,如图所示:
当项目在挂载到服务器上,平均都是10S+以上加载出来,好家伙这加载时间,仿佛过了半个世纪,很烦人,心态boom, 开发者甚至都有种想砸电脑的冲动
Vue中计算白屏时间、首屏加载时间
// 白屏时间
// Vite环境下Vue项目的main.js或入口文件中引入这段代码
if ("performance" in window) {
const perf = performance;
let fpTime;
if (perf.getEntriesByName) {
const entry = perf.getEntriesByName("first-paint")[0];
if (entry) {
fpTime = entry.startTime;
}
} else if (window.performance.timing) {
// 对不支持Performance Paint Timing API的老版本浏览器,可以尝试使用navigationStart和firstPaint
fpTime = perf.timing.firstPaintTime;
// 若老版浏览器也不支持,则可考虑自定义测量方式,如设置pageStartTime
}
if (fpTime) {
// fpTime即为白屏时间,需要转换为相对时间戳(相对于navigationStart)
const whiteScreenTime = fpTime - perf.timing.navigationStart;
console.log("白屏时间: ", whiteScreenTime.toFixed(2), "ms");
}
}
// 首屏加载时间
// 计算First Contentful Paint (FCP)
let fcpTime;
if (perf.getEntriesByName) {
const entry = perf.getEntriesByName("first-contentful-paint")[0];
if (entry) {
fcpTime = entry.startTime;
}
}
if (fcpTime) {
const firstScreenTime = fcpTime - perf.timing.navigationStart;
console.log("首屏时间: ", firstScreenTime.toFixed(2), "ms");
}
一、分析下前端加载速度慢原因
第一步:首先安装webpack的可视化资源分析工具,命令行执行:
npm i webpack-bundle-analyzer -D
第二步:然后在webpack的dev开发模式配置中,引入插件,代码如下:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
plugins: [
new BundleAnalyzerPlugin()
]
第三步:最后命令行执行 npm run build --report
, 浏览器会自动打开分析结果,如下所示:
可以看到vue全家桶相关依赖占用了很大的空间,对webpack的构建速度和网站加载速度都会有比较大的影响。单页应用会随着项目越大,导致首屏加载速度很慢,针对目前所暴露出来的问题,有以下几种优化方案可以参考:
二、优化方案
常见的几种SPA首屏优化方式
-
减小入口文件积
-
开启GZip压缩
-
静态资源本地缓存
-
UI框架按需加载
-
图片资源的压缩
-
采用第三方cdn资源
-
代码层面的优化
0.初步优化
初步优化,减少全局组件引入(是否放在main.js),按需引入需要的模块(echarts按需引入等),使用轻量级数据库(moment.js 切换 data-fns等)
1.采用异步组件和懒加载的方式
异步组件/组件懒加载:
这样做的目的是在首屏渲染时减少加载文件的数量,在需要用到一些组件时才会从服务器获取,而且不会重复请求,可以有效的减少白屏的时间。
组件重复打包
假设A.js
文件是一个常用的库,现在有多个路由使用了A.js
文件,这就造成了重复下载
解决方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks:3,
minChunks
为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
路由懒加载:
SPA 项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面打包成一个文件,「当用户打开首页时,会一次性加载所有的资源」,造成首页加载很慢,降低用户体验
访问到当前页面才会加载相关的资源,异步方式分模块加载文件,默认的文件名是随机的id。如果在output中配置了chunkFilename,可以在component中添加WebpackChunkName,是为了方便调试,在页面加载时候,会显示加载的对应 文件名.hash 值,如下图:
{
path: '/Login',
name: 'Login',
component: () = >import( /* webpackChunkName: "Login" */ '@/view/Login')
}
路由懒加载的原理:
懒加载前提的实现:ES6 的动态地加载模块——
import()
图片懒加载:使用vue-lazyload插件
Vue 图片懒加载 之 Vue-Lazyload-CSDN博客
//引入vue懒加载
import VueLazyload from 'vue-lazyload'
//方法一: 没有页面加载中的图片和页面图片加载错误的图片显示
// Vue.use(VueLazyload)
//方法二: 显示页面图片加载中的图片和页面图片加载错误的图片
//引入图片
import loading from '@/assets/images/load.jpg'
//注册图片懒加载
Vue.use(VueLazyload, {
// preLoad: 1.3,
error: '@/assets/images/error.jpg',//图片错误的替换图片路径(可以使用变量存储)
loading: loading,//正在加载的图片路径(可以使用变量存储)
// attempt: 1
})
使用:
<div class="lazyLoad">
<ul>
<li v-for="img in arr">
<img v-lazy="img.thumbnail_pic_s">
</li>
</ul>
</div>
2.webpack开启gzip压缩文件传输模式
gizp压缩是一种http请求优化方式,通过减少文件体积来提高加载速度。html、js、css文件甚至json数据都可以用它压缩,可以减小60%以上的体积。
前端配置gzip压缩,并且服务端使用nginx开启gzip,用来减小网络传输的流量大小。
webpack打包时借助 compression webpack plugin实现gzip压缩,安装插件如下:
npm i compression-webpack-plugin
或
npm install compression-webpack-plugin@6.1.1 --save-dev
在vue-cli 3.0 中,vue.config.js配置如下:
const CompressionPlugin = require('compression-webpack-plugin');//引入gzip压缩插件
module.exports = {
plugins:[
new CompressionPlugin({//gzip压缩配置
filename: '[path][base].gz',
algorithm: 'gzip', // 压缩算法,官方默认压缩算法是gzip
test:/\.js$|\.css$|\.html$|\.eot$|\.woff$/,// 使用gzip压缩的文件类型
threshold:10240,//对超过10kb的数据进行压缩,默认是10240
deleteOriginalAssets:false,//是否删除原文件
minRatio: 0.8, // 最小压缩比率,默认是0.8
})
]
}
启用gzip压缩打包之后,会变成下面这样,自动生成gz包。目前大部分主流浏览器客户端都是支持gzip的,就算小部分非主流浏览器不支持也不用担心,不支持gzip格式文件的会默认访问源文件的,所以不要配置清除源文件。
在nginx中开启gzip:
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;
}
配置好之后,打开浏览器访问线上,F12查看控制台,如果该文件资源的响应头里显示有Content-Encoding: gzip,表示浏览器支持并且启用了Gzip压缩的资源
3.Webpack 代码分割与优化
模块拆分: 配置Webpack将代码拆分成多个小块,利用Tree Shaking、代码压缩等技术减少代码体积。这将减少初始加载所需的下载时间,提高页面加载速度。
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 30000,
}
}
}
};
4.依赖模块采用第三方cdn资源(对于第三方js库的优化,分离打包)
生产环境是内网的话,就把资源放内网,通过静态文件引入,会比node_modules和外网CDN的打包加载快很多。如果有外网的话,可以通过CDN方式引入,因为不用占用访问外网的带宽,不仅可以为您节省流量,还能通过CDN加速,获得更快的访问速度。但是要注意下,如果你引用的CDN 资源存在于第三方服务器,在安全性上并不完全可控。国内的CDN服务推荐使用 BootCDN
目前采用引入依赖包生产环境的js文件方式加载,直接通过window可以访问暴露出的全局变量,不必通过import引入,Vue.use去注册
在webpack的dev开发配置文件中, 加入如下参数,可以分离打包第三方资源包,key为依赖包名称,value是源码抛出来的全局变量。对于一些其他的工具库,尽量采用按需引入的方式。
使用 CDN 的好处有以下几个方面
(1)加快打包速度。分离公共库以后,每次重新打包就不会再把这些打包进 vendors 文件中。
(2)CDN减轻自己服务器的访问压力,并且能实现资源的并行下载。浏览器对 src 资源的加载是并行的(执行是按照顺序的)。
第一步:修改vue.config.js
module.exports = {
...
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'axios': 'axios',
'element-ui': 'ELEMENT',
'underscore' : {
commonjs: 'underscore',
amd: 'underscore',
root: '_'
},
'jquery': {
commonjs: 'jQuery',
amd: 'jQuery',
root: '$'
}
}
...
}
如果想引用一个库,但是又不想让webpack打包,且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用,那就可以通过配置externals
第二步:在index.html中添加cdn
<link href="https://cdn.bootcss.com/element-ui/2.7.2/theme-chalk/index.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.4/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.bootcss.com/element-ui/2.7.2/index.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore-min.js"></script>
</body>
第三步:去除vue.use相关代码
通过 CDN 引入,在使用 VueRouter Vuex ElementUI 的时候要改下写法。CDN会把它们挂载到window上,可以不再使用Vue.use(xxx)
main.js中 注释掉
// import Vue from 'vue';
// import iView from 'iview';
// import '../theme/index.less';
5.生产环境禁止生成map文件
在vue.config.js配置:
module.exports = {
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境是否生成 sourceMap 文件,一般情况不建议打开
}
在设置了productionSourceMap: false之后,就不会生成map文件,map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。也就是说map文件相当于是查看源码的一个东西。如果不需要定位问题,并且不想被看到源码,就把productionSourceMap 置为false,既可以减少包大小,也可以加密源码。
6.去掉代码中的console和debugger
打包之后控制台很干净,部署正式环境之前最好这样做。vue-cli3.0
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
config.optimization.minimizer[0].options.terserOptions.compress.warnings = false
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
config.optimization.minimizer[0].options.terserOptions.compress.drop_debugger = true
config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = ['console.log']
}
},
uglifyOptions去除console来减少文件大小
// 安装uglifyjs-webpack-plugin
cnpm install uglifyjs-webpack-plugin --save-dev
// 修改vue.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
return {
plugins: [
//打包环境去掉console.log
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false,
drop_console: true, //注释console
drop_debugger: true, //注释debugger
pure_funcs: ['console.log'], //移除console.log
},
},
}),
],
}
}
}
}
7. 预渲染配置
config.plugins.delete('preload')
config.plugins.delete('prefetch')
使用插件:prerender-spa-plugin
vue.config.js中配置如下:
const PrerenderSpaPlugin = require('prerender-spa-plugin');
const Render = PrerenderSpaPlugin.PuppeteerRenderer;
const path = require('path');
configureWebpack: () => {
if (process.env.NODE_ENV !== 'production') return;
return {
plugins: [
new PrerenderSPAPlugin({
// 生成文件的路径,也可以与webpakc打包的一致。
// 下面这句话非常重要!!!
// 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
staticDir: path.join(__dirname, 'dist'),
// 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
routes: ['/', '/Login', '/Home'],
// 这个很重要,如果没有配置这段,也不会进行预编译
renderer: new Renderer({
inject: {
foo: 'bar'
},
headless: false,
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
renderAfterDocumentEvent: 'render-event'
})
})
]
};
}
提前加载: Vue 3的路由支持预加载功能,可在用户浏览站点时预先加载下一个页面所需的资源。这将确保用户切换页面时的迅速加载和呈现。
const routes = [
{
path: '/home',
component: () => import('./Home.vue'),
meta: { preload: true }
},
// 其他路由...
];
8.图片资源的压缩、icon资源使用、雪碧图、代码压缩
严格说来这一步不算在编码技术范围内,但是却对页面的加载速度影响很大。对于所有的图片文件,都可以在一个叫tinypng的网站上去压缩一下。网址:tinypng.com/,对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。然后通过操作CSS的background属性,控制背景的位置以及大小,来展示需要的部分。
// 图片压缩设置
chainWebpack: config => {
// 图片打包压缩,使用了 --- image-webpack-loader --- 插件对图片进行压缩
config.module
.rule('images')
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({ bypassOnDebug: true })
.end()
},
使用字体图标
字体图标的优点:
1)轻量级:一个图标字体要比一系列的图像要小。一旦字体加载了,图标就会马上渲染出来,减少了 http 请求
2)灵活性:可以随意的改变颜色、产生阴影、透明效果、旋转等
3)兼容性:几乎支持所有的浏览器,请放心使用
图片转 base64 格式
将小图片转换为 base64 编码字符串,并写入 HTML 或者 CSS 中,减少 http 请求
转 base64 格式的优缺点:
1)它处理的往往是非常小的图片,因为 Base64 编码后,图片大小会膨胀为原文件的 4/3,如果对大图也使用 Base64 编码,后者的体积会明显增加,即便减少了 http 请求,也无法弥补这庞大的体积带来的性能开销,得不偿失
2)在传输非常小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 http 请求开销相比,可以忽略不计,这时候才能真正体现出它在性能方面的优势
项目可以使用 url-loader
将图片转 base64:
// 安装
npm install url-loader --save-dev
// 配置
module.exports = {
module: {
rules: [{
test: /.(png|jpg|gif)$/i,
use: [{
loader: 'url-loader',
options: {
// 小于 10kb 的图片转化为 base64
limit: 1024 * 10
}
}]
}]
}
};
js代码压缩- - - -(webpack 自UglifyJsPlugin插件压缩js文件)
css 代码压缩- - - - (采用optimize-css-assets-webpack-plugin插件来压缩css代码)
9. 前端页面代码层面的优化
-
合理使用v-if和v-show
-
合理使用watch和computed
-
使用v-for必须添加key, 最好为唯一id, 避免使用index, 且在同一个标签上,v-for不要和v-if同时使用
-
定时器的销毁。可以在beforeDestroy()生命周期内执行销毁事件;也可以使用$once这个事件侦听器,在定义定时器事件的位置来清除定时器。详细见vue官网
-
长列表性能优化
- 图片资源懒加载
- 前端接口防止重复请求实现方案-CSDN博客
- 用innerHTML代替dom操作,减少dom操作的次数,优化js性能
- 合理使用requestAnimationFrame动画代替setTimeOut
- 通过创建文档碎片 document.createDocumentFragment()-创建虚拟dom来更新dom
-
合理使用 Tree shaking
合理使用 Tree shaking
在webpack中,可以通过在配置文件中设置 optimization.minimize 为true来开启Tree Shaking。
Tree shaking 的作用:消除无用的 JS 代码,减少代码体积
例如:
// util.js
export function targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
项目中只使用了 targetType 方法,但未使用 deepClone 方法,项目打包后,deepClone 方法不会被打包到项目里
tree-shaking 原理:
依赖于 ES6 的模块特性,ES6 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking 的基础
静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。ES6 之前的模块化,比如 CommonJS 是动态加载,只有执行后才知道引用的什么模块,就不能通过静态分析去做优化,正是基于这个基础上,才使得 tree-shaking 成为可能
Tree shaking 并不是万能的
并不是说所有无用的代码都可以被消除,还是上面的代码,换个写法 tree-shaking 就失效了
// util.js
export default {
targetType(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
},
deepClone(target) {
return JSON.parse(JSON.stringify(target));
}
};
// 引入并使用
import util from '../util';
util.targetType(null)
同样的,项目中只使用了 targetType 方法,未使用 deepClone 方法,项目打包后,deepClone 方法还是被打包到项目里
在 dist 文件中搜索 deepClone 方法:
究其原因,export default 导出的是一个对象,「无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效」
这也是函数式编程越来越火的原因,因为可以很好利用 tree-shaking 精简项目的体积,也是 vue3 全面拥抱了函数式编程的原因之一
静态资源本地缓存
后端返回资源问题:
-
采用
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头 -
采用
Service Worker
离线缓存
前端合理利用localStorage
10.解决白屏,体验优化
把上面所有的优化都做完之后,加载速度有了显著提升,但是在网慢的时候还是会有白屏,所以再白屏期间加骨架屏和loading就显得格外重要了。
当我们的js加载完成之后我们的img就会被覆盖了,所以这种效果是特别好的,因为静态资源是在我们的项目当中直接有的,所以加载效率会特别的快,所以尽量让图片的大小变小这样会更好的提高项目效率
骨架屏插件
这里以 vue-skeleton-webpack-plugin
插件为例,该插件的亮点是可以给不同的页面设置不同的骨架屏,这点确实很酷
1)安装
npm i vue-skeleton-webpack-plugin
2)vue.config.js 配置
// 骨架屏
const SkeletonWebpackPlugin = require("vue-skeleton-webpack-plugin");
module.exports = {
configureWebpack: {
plugins: [
new SkeletonWebpackPlugin({
// 实例化插件对象
webpackConfig: {
entry: {
app: path.join(__dirname, './src/skeleton.js') // 引入骨架屏入口文件
}
},
minimize: true, // SPA 下是否需要压缩注入 HTML 的 JS 代码
quiet: true, // 在服务端渲染时是否需要输出信息到控制台
router: {
mode: 'hash', // 路由模式
routes: [
// 不同页面可以配置不同骨架屏
// 对应路径所需要的骨架屏组件id,id的定义在入口文件内
{ path: /^\/home(?:\/)?/i, skeletonId: 'homeSkeleton' },
{ path: /^\/detail(?:\/)?/i, skeletonId: 'detailSkeleton' }
]
}
})
]
}
}
3)新建 skeleton.js 入口文件
// skeleton.js
import Vue from "vue";
// 引入对应的骨架屏页面
import homeSkeleton from "./views/homeSkeleton";
import detailSkeleton from "./views/detailSkeleton";
export default new Vue({
components: {
homeSkeleton,
detailSkeleton,
},
template: `
<div>
<homeSkeleton id="homeSkeleton" style="display:none;" />
<detailSkeleton id="detailSkeleton" style="display:none;" />
</div>
`,
});
补充知识:
先来说一下哈希值的不同:
- hash 是 build-specific ,即每次编译都不同——适用于开发阶段
- chunkhash 是 chunk-specific,是根据每个 chunk 的内容计算出的 hash——适用于生产
所以,在生产环境,要把文件名改成'[name].[chunkhash]',最大限度的利用浏览器缓存。
output: {
...........
chunkFilename: "[name].[chunkhash:5].chunk.js",
},