首屏速度是用户体验的最关键一环,而首屏速度最大的决定性因素是资源的加载速度。
而资源加载的速度=资源大小(重点)+网速
在Vue项目中,引入到工程中的所有js、css等文件,编译时都会被打包进vendor.js,浏览器在加载该文件之后才能开始显示首屏。若是引入的库众多,那么vendor.js文件体积将会相当的大,影响首屏的体验。可以看个例子:
这是优化前的页面加载状态:执行 npm run build
打包项目,出来的vendeor.js文件,基本都是1M以上的的巨大文件,没有用户能忍受5s以上的loading而不关闭页面的,如图所示:
测量vue首屏加载速度 - 使用开发者工具
Chrome开发者工具
-
打开开发者工具:在Chrome浏览器中,按F12或右键点击页面选择“检查”,打开开发者工具。
-
记录首屏加载时间:
刷新页面。
在Network标签页中,可以看到加载资源的瀑布图。
观察DOMContentLoaded
事件和load
事件的时间。DOMContentLoaded
事件通常标志着DOM的构建完成,而load
事件表示所有资源(包括图片、样式表等)都已加载完成。
当项目在挂载到服务器上,平均都是5s+以上加载出来,好家伙这加载时间,仿佛过了半个世纪,很烦人,心态boom, 开发者甚至都有种想砸电脑的冲动
Vue中计算白屏时间、首屏加载时间
方式一:使用performance.timing
// 白屏时间
// 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");
}
}
// 测量首屏加载时间
if ('performance' in window) {
const t = performance.timing;
const loadTime = t.loadEventEnd - t.navigationStart;
console.log(`首屏加载时间: ${loadTime} 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");
}
方式二:使用Lighthouse
Lighthouse是一个开源的自动化工具,用于改善网页的质量。它可以帮助你发现性能瓶颈、可访问性问题和SEO问题。
-
访问Lighthouse:在Chrome开发者工具中,点击“Lighthouse”标签。
-
运行测试:选择你想要测试的URL,点击“生成报告”。
-
查看结果:在报告中,你会看到关于性能、可访问性、最佳实践和SEO的评分和建议。
方式三:使用Web Vitals库
Web Vitals是一套核心的网站性能指标,包括LCP(最大内容绘制时间)、FID(首次输入延迟)和CLS(累积布局偏移)。这些指标对于用户体验至关重要。你可以使用Chrome的用户体验报告来监控这些指标。
有一些Vue插件或库可以帮助你更简单地获取和监控页面的性能数据,例如vue-performance
。这些插件通常提供了更直观的API来获取和展示性能数据。
npm install web-vitals --save-dev
import { getCLS, getFID, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// 发送到分析工具,如Google Analytics等
console.log(metric); // 临时打印或发送到你的分析系统
}
getCLS(sendToAnalytics); // 累积布局偏移量(Cumulative Layout Shift)
getFID(sendToAnalytics); // 首次输入延迟(First Input Delay)
getLCP(sendToAnalytics); // 最大内容绘制(Largest Contentful Paint)
getTTFB(sendToAnalytics); // 首次字节到首次绘制(Time to First Byte)
一、分析下前端加载速度慢原因
第一步:首先安装webpack的可视化资源分析工具,命令行执行:
npm i webpack-bundle-analyzer -D
第二步:然后在webpack的dev开发模式配置中,引入插件,代码如下:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
plugins: [
new BundleAnalyzerPlugin()
]
第三步:最后命令行执行 npm run build --report
, 浏览器会自动打开分析结果,如下所示:
可以看到vue全家桶相关依赖占用了很大的空间,对webpack的构建速度和网站加载速度都会有比较大的影响。单页应用会随着项目越大,导致首屏加载速度很慢,针对目前所暴露出来的问题,有以下几种优化方案可以参考:
二、优化方案
优化分类:
- 网络优化
- 代码优化
- webpack、压缩体积优化
- 资源优化
常见的几种SPA首屏优化方式
-
减小入口文件积;UI框架按需加载
-
代码层面的优化 - 代码尽量精简
-
减少dom操作,减少重排
-
移除不再需要的事件监听,防止内存泄漏
-
网络请求优化
-
开启Brotli,GZip压缩
-
图片资源的压缩
-
采用第三方CDN资源
-
资源加载优化
-
利用浏览器缓存;静态资源本地缓存
0.初步优化
初步优化:减少全局组件、插件的引入(是否放在main.js),按需引入需要的模块(echarts按需引入等),使用轻量级数据库(moment.js 切换 data-fns等)
使用时引入插件:在组件mounted阶段再引入库,或者用到这个功能时再引入
一些非马上用的操作改成异步:
1.采用异步组件和懒加载的方式
异步组件
这样做的目的是在首屏渲染时减少加载文件的数量,在需要用到一些组件时才会从服务器获取,而且不会重复请求,可以有效的减少白屏的时间。
懒加载第三方库(例如使用webpack的动态导入)
// 在需要的时候才加载库
import('some-library').then(library => {
// 使用library
});
路由懒加载
{
path: '/Login',
name: 'Login',
component: () => import('@/view/Login')
}
组件重复打包
假设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-优快云博客
//引入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" :key="img.id">
<img v-lazy="img.thumbnail_pic_s">
</li>
</ul>
</div>
合理地分割代码模块,不仅可以使代码结构更加清晰,还能够提高加载效率。只加载用户当前需要的部分,避免一次性加载整个应用的所有代码。
2.webpack开启gzip压缩文件传输模式
Gzip 是一种用于文件压缩与解压缩的文件格式。它基于 Deflate 算法,可将文件(译者注:快速地、流式地)压缩地更小,从而实现更快的网络传输。Web 服务器与现代浏览器普遍地支持 Gzip,这意味着服务器可以在发送文件之前自动使用 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,
}
}
}
};
---改变webpack的输出结果
其实webpack关于缓存方面的功能,提供了很多功能强大的插件。
- 比如CommonsChunkPlugin可以用来在打包的时候提取公共js代码,还有其他插件
- 使用HappyPack多线程加速loader
- ExtractTextPlugin可以用来从js中提出css,将其输出到一个独立的文件
- 能够将我们打包的精度加以划分,将公共引用的部分打包为一个单独的文件
- 使用chunkhash替代hash,根据模块内容来添加hash,只要文件没有改变,就不会生成新的hash
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. 资源加载优化 prefetch(预加载)、preload(按需加载,默认)
prefetch会等到preload的资源加载后在加载
config.plugins.delete('preload') 关掉preload(默认是preload加载)
config.plugins.delete('prefetch') 关掉prefetch
vue.config.js 中全局配置中关了prefetch,某个组件开启prefetch 如下:
webpackPrefetch: true 同时加载
webpackPrefetch: 数值 加载的优先级,数值越大优先级越高
使用插件: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:
// 需先安装image-webpack-loader
// npm install image-webpack-loader --save-dev
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000, // 图片小于10k的转为base64格式,超过 file-loader处理
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',// 压缩图片
options: {
bypassOnDebug: true,
}
}
]
}
需要注意一点,上面说到如果图片小于10k将转为base64格式。这里判断的图片大小是经过
image-webpack-loader
压缩后的图片大小。
使用cdn服务器单独存储图片
js代码压缩- - - -(webpack 自UglifyJsPlugin插件压缩js文件)
module.exports = {
optimization: {
minimizer: [ // 自定义压缩器
new UglifyJsPlugin({
uglifyOptions: {
output: {
comments: false, // 删除所有注释
},
},
}),
],
},
};
css 代码压缩- - - - (采用optimize-css-assets-webpack-plugin插件来压缩css代码)
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
discardComments: {
removeAll: true,
},
},
}),
],
},
};
压缩HTML---使用html-webpack-plugin
压缩HTML
// 在webpack.config.js里使用
const HtmlWebpackPlugin = require('html-webpack-plugin')
//code
plugins:[
new HtmlWebpackPlugin({
minify:{
//是否对大小写敏感,默认false
caseSensitive: true,
//是否简写boolean格式的属性如:disabled="disabled" 简写为disabled 默认false
collapseBooleanAttributes: true,
//是否去除空格,默认false
collapseWhitespace: true,
//是否压缩html里的css(使用clean-css进行的压缩) 默认值false;
minifyCSS: true,
//是否压缩html里的js(使用uglify-js进行的压缩)
minifyJS: true,
//Prevents the escaping of the values of attributes
preventAttributesEscaping: true,
//是否移除属性的引号 默认false
removeAttributeQuotes: true,
//是否移除注释 默认false
removeComments: true,
//从脚本和样式删除的注释 默认false
removeCommentsFromCDATA: true,
//是否删除空属性,默认false
removeEmptyAttributes: true,
// 若开启此项,生成的html中没有 body 和 head,html也未闭合
removeOptionalTags: false,
//删除多余的属性
removeRedundantAttributes: true,
//删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
removeScriptTypeAttributes: true,
//删除style的类型属性, type="text/css" 同上
removeStyleLinkTypeAttributes: true,
//使用短的文档类型,默认false
useShortDoctype: true
},
hash:true, // 加入哈希来禁止缓存
template:'./src/index.html', // 源模板
filename:'assets/admin.html' // 编译后的文件及路径
})
]
9. 前端页面代码层面的优化
-
合理使用v-if和v-show
-
合理使用watch和computed
-
使用v-for必须添加key, 最好为唯一id, 避免使用index, 且在同一个标签上,v-for不要和v-if同时使用
-
定时器的销毁。可以在beforeDestroy()生命周期内执行销毁事件;也可以使用$once这个事件侦听器,在定义定时器事件的位置来清除定时器。详细见vue官网
-
长列表性能优化
- 图片资源懒加载
- 前端接口防止重复请求实现方案-优快云博客
- 用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 全面拥抱了函数式编程的原因之一
10.利用浏览器缓存;静态资源本地缓存
后端返回资源问题:
-
采用
HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头 -
采用
Service Worker
离线缓存
前端合理利用localStorage
11.首屏数据尽量并行,如果可行让小数据量接口合并到其他接口
12.静态资源尽量用多个子域名
13.vue项目中多 import 优化
在Vue项目中进行多 import 优化,主要是为了减少初始加载时间,通过代码分割等方式来按需加载组件和库。以下是一些优化的策略和示例代码:
1 使用异步组件
const AsyncComp = () => import('./AsyncComp.vue');
export default {
components: {
AsyncComp
}
}
2 使用webpack的代码分割(code splitting)特性
// 在webpack.config.js中配置
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
3 按需加载第三方库(例如使用webpack的动态导入)
// 在需要的时候才加载库
import('some-library').then(library => {
// 使用library
});
4 使用vue-router的懒加载
const Foo = () => import('./Foo.vue');
const Bar = () => import('./Bar.vue');
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
});
5 使用懒加载时结合webpack的magic comment
// 使用webpack提供的magic comment
const SomeLibrary = () => import(/* webpackChunkName: "group-name" */ 'some-library');
6 使用按需加载时结合webpack的预加载
webpackPrefetch
// 使用webpack提供的webpackPrefetch
const SomeLibrary = () => import(/* webpackChunkName: "group-name" */ /* webpackPrefetch: 10 */ 'some-library');
14.解决白屏,体验优化
把上面所有的优化都做完之后,加载速度有了显著提升,但是在网慢的时候还是会有白屏,所以再白屏期间加骨架屏和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>
`,
});
补充知识:
1.Webpack中的[name]、[chunkhash]、[id]
在Webpack中,你可以通过配置output.filename来给输出的文件名添加hash。Webpack支持多种hash算法,包括md5, sha256, sha1, 和 sha512。
先来说一下哈希值的不同:
- hash 是 build-specific 项目hash,即每次编译都不同——适用于开发阶段
- chunkhash:基于整个chunk的内容生成哈希值。一个chunk通常包括多个模块,甚至可能包括其他chunk。因此,当某个chunk的内容发生变化时,整个chunk的文件名都会发生变化。
chunkhash 适合用于那些将代码分割成多个 chunk 的项目(如使用代码分割、懒加载等技术)。它可以更细粒度地控制缓存,因为只有改变了的 chunk 会失去缓存。 - contenthash:基于单个文件内容生成哈希值。它只关注文件的内容,不管这个文件是由哪个chunk生成的。因此,只有当文件内容发生变化时,文件名才会发生变化。
contenthash 特别适合用于样式文件(如 CSS)或者你的项目中任何可以独立缓存的资源。
所以,在生产环境,要把文件名改成'[name].[chunkhash]',利用内容哈希解决浏览器缓存问题。
使用chunkhash替代hash,根据模块内容来添加hash,只要文件没有改变,就不会生成新的hash
// webpack.config.js
const path = require('path');
module.exports = {
// ...
output: {
...........
// 使用[contenthash]来生成唯一的文件名
filename: "[name].[contenthash].js", // 使用contenthash而非chunkhash,适用于小文件
path: path.resolve(__dirname, 'dist'),
// 对于代码分割的chunk文件也应用相同的策略
chunkFilename: '[name].[contenthash].chunk.js',
},
// ...
};
在这个配置中,[name]
是文件的名字(通常是入口点的名字),[contenthash]
是文件内容的hash,它会在文件内容变化时变化,从而确保缓存的有效性。
如果你想要使用chunkhash,它会根据每个chunk(bundle)的内容计算hash,适用于大文件:
// webpack.config.js
const path = require('path');
module.exports = {
// ...
output: {
filename: '[name].[chunkhash:8].js', // 使用chunkhash;8 表示hash值的长度
path: path.resolve(__dirname, 'dist'),
},
// ...
};
2.Brotli 的性能比Gzip提高了 17-25%
Brotli 压缩只在 https 下生效,因为在 http 请求中 request header里的 Accept-Encoding 只有gzip,deflate。并且 Brotli 和 Gzip 是可以并存的,因此无需关闭gzip,客户端自行判断压缩算法
使用的前提
在 Chrome 中只有 https 的网站发起的请求头 Accept-Encoding 中才会加上 br 选项,此时服务端才会响应 brotli 压缩的资源。因此只有网站支持 https 时,开启 br 压缩才有价值
1、Brotli 简介
Brotli 是 Google 推出的一种无损压缩算法,通过变种的LZ77算法、Huffman编码以及等方式进行数据压缩。Brotli压缩数据格式的规范在RFC 7932中定义。与其他压缩算法相比(如zip,gzip等),无论是压缩时间,还是压缩体积上看,它都有着更高的效率。
Brotli压缩支持单文件
- 支持的文件类型: text/xml、text/plain、text/css、text/javascript、application/javascript、application/x-javascript、application/rss+xml、application/json、application/xml、image/tiff、image/svg+xml
- 可结合tar, 达到支持文件夹
2、Brotli优势
HTTP的gzip压缩可节省网络流量,提升网络速度,但gzip压缩率还不够高,通过调研,有大量算法压缩率都比gzip高。综合压缩率和实现成本,brotli压缩算法落地成本最低,且业界字节app已有多个域名使用brotli算法。
brotli有11个级别,通过测试发现选用中间级别—7级别比较合适,brotli相对gzip消耗的CPU和内存资源更多,但在可接受范围内。
参考链接:https://juejin.cn/post/7194627379656917047
3、Brotli接入到android
对于 web 和 ios 端,在浏览器和系统层已经开始支持 br 的自动解压了,但 Android 系统不会自动解析 br 数据,需要在应用层自己去实现。
Okhttp 中发现了 BrotliInterceptor 的拦截器,但它是 Brotli 库的 Java 描述,没有真正的 Java 编码器实现。
1. 首先请求头中加入Content-Encoding:br
val request = chain.request().newBuilder()
.header("Accept-Encoding", "br,gzip")
.build()
2. OkHttp请求的时候添加拦截器
- 直接使用square公司的拦截器
implementation("com.squareup.okhttp3:okhttp-brotli:4.1.0")
OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(BrotliInterceptor.INSTANCE) .build();
- 依赖implementation("org.brotli:dec:0.1.2”),自行实现拦截器
implementation("org.brotli:dec:0.1.2") //https://mvnrepository.com/artifact/org.brotli/dec
拦截器
object BrotliInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { return if (chain.request().header("Accept-Encoding") == null) { val request = chain.request().newBuilder() .header("Accept-Encoding", "br,gzip") .build() val response = chain.proceed(request) uncompress(response) } else { chain.proceed(chain.request()) } } fun uncompress(response: Response): Response { if (!HttpHeaders.hasBody(response)) { return response } val body = response.body() ?: return response val encoding = response.header("Content-Encoding") ?: return response val decompressedSource = when { encoding.equals("br", ignoreCase = true) -> BrotliInputStream(body.source().inputStream()).source().buffer() encoding.equals("gzip", ignoreCase = true) -> GzipSource(body.source()).buffer() else -> return response } return response.newBuilder() .removeHeader("Content-Encoding") .removeHeader("Content-Length") .body(decompressedSource.asResponseBody(body.contentType(), -1)) .build() } }
4、怎么使用Brotli压缩
怎样知道浏览器是否支持 Brotli?
当打开一个网页,然后在浏览器使用期开发工具的时候,可以查看其 content-encoding,如果看到 br 字样,那就是Brotli,如果看到 gzip,那就是用的 gzip 方式。