全文共6511字/词,阅读大概须要13分钟,太长不看党请直接移步👉「开始优化」部分直接查看优化手段javascript
前段时间公司服务器网络波动,网站访问变慢,一些性能问题也随之暴露了出来。纷纷反馈在这样的弱网条件下,访问新项目时,加载了近1分钟都没加载出来,而访问其余页面顶多也就30-40s。css
在网络恢复后,尝试访问了下页面,无缓存首次打开须要等待近11s的时间,最大的资源达到了3.7M...html
在对项目作了一些优化处理后,再次无缓存打开能够发现网页几乎是秒开,平均耗时在1s之内前端
在这里总结记录一下,基本上都是一些常规可复制的优化手段,但愿能为一样想优化网页性能的你提供思路~vue
Slow3G条件下22-25s加载完成java
在开始以前,咱们须要明白一个原则:性能优化的最终目的是提高用户体验。
简而言之就是让用户感受这个网站很「快」(至少不慢hh),这里的「快」有两种,一种是「真的快」一种是「以为快」node
作好这两方面都能提高用户对网站的性能评价。webpack
另外就是软件工程没有银弹,一种优化方案可能适用于大多数项目,可是某些特殊状况下极可能会起反效果。nginx
举个🌰,因为浏览器有单域名下并发请求限制,一般咱们会将依赖统一打成一个vendor包(vue-cli默认策略),减小首屏请求数,且依赖不变更的状况下文件指纹不变,能够有效利用304缓存。在依赖很少的状况这么处理确实有助于提高加载速度,但一旦依赖多起来,vendor就会特别的大,在弱网条件下,会严重拖慢页面显示。这显然不是咱们想要的,因此咱们根据状况会对vendor进行拆分,好比拆分到CDN,或者直接拆分到页面中git
所以,咱们在作性能优化过程当中,必须根据最终能给用户体验带来的提高权衡后作出适合当前项目的选择
目标会影响咱们在过程当中的决策
指标则用来度量咱们的目标
首先咱们须要肯定目标,根据场景和项目复杂度不一样,制定的目标也不一样,好比但愿比竞品快20%,或者符合标准的"2-5-10"原则等等
这里我定下的目标是
关于指标这块,简单介绍下常见指标
经过性能调试工具能够直观便捷地获取这些指标,好比Newwork、k六、hiper、Lighthouse...。具体能够看我关于性能调试工具的另外一篇文章
从Network上咱们发现主要问题在3.2M的chunk-vendor.js上
因为本次不涉及到应用内场景性能优化,Performance分析跳过...
从webpack bundle能够看出,问题着实很多
🛫🛫🛫
before:4.96M after:4.12M
每次使用在线服务手动压缩较为麻烦,能够直接在构建流程中加入压缩图片
// install
npm i image-webpack-loader -D
// vue.config.js
chainWebpack: (config) => {
if (isProd) {
// 图片压缩处理
const imgRule = config.module.rule('images')
imgRule
.test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({ bypassOnDebug: true })
.end()
}
}
复制代码
- install或build时若是出现imagemin库下载失败,能够尝试 换源、配置github hosts、install时添加
--user=root
解决- 因为在图片下载后已经手动用在线工具压缩过,这部分提高不大
before:4.12M after:4.00M
webP是谷歌推出的新图片格式(2010),同等质量下体积拳打png脚踢jpg,目前兼容性还算能够,就苹果家的表现不太理想
能够手动,也能够加入构建自动化生成。
./cwebp -q 75 login_plane_2.png -o login_plane_2.webp
复制代码
<picture>
标签兼容<picture>
<source srcset="hehe.webp" type="image/webp">
<img src="hehe.png" alt="hehe>
</picture>
复制代码
// main.js
window.addEventListener('DOMContentLoaded', () => {
const isSupportWebP = document.createElement('canvas')
.toDataURL('image/webp')
.indexOf('data:image/webp') === 0
document.documentElement.classList.add(isSupportWebP ? '' : '.no-support-webp');
})
// css
.support-webp .bg{
background-image: url("hehe.webp");
}
.no-support-webp .bg {
background-image: url("hehe.png");
}
复制代码
- 请务必使用原图进行webp转换,不然会影响体积
- 项目大图很少,最大400KB的图片转换后只有48.9KB
这一步咱们来优化部分冗余的旧SVG图标被打包进去的状况,通常项目中SVG使用方式都是在iconfont生成JS而后引入。这种作法
要实现上述效果,只须要
// install
npm i svg-sprite-loader -D
// vue.config.js
chainWebpack: (config) => {
// SVG处理
config.module
.rule('svg')
.exclude.add(resolve('src/icons/svg'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons/svg'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
}
复制代码
// src/icons/index.js
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
// main.js
import '@/icons'
复制代码
// src/icons/index.js
import Vue from 'vue'
import CaSVG from '@/components/ca-svg'
Vue.component('ca-svg', CaSVG)
// src/components/ca-svg.vue
<template>
<svg :class="svgClass" aria-hidden="true" v-on="$listeners" :style="svgStyle">
<use :xlink:href="iconName" />
</svg>
</template>
...
// name属性为必须字段,其余size或color能够自由定制
复制代码
SVG一般会有一些冗余信息致使影响体积,这里咱们可使用svgo-loader来进一步压缩
// install
npm i svgo-loader -D
// vue.config.js
// 接上面svg的配置
...
.end()
.use('svgo-loader')
.loader('svgo-loader')
.end()
复制代码
能够看到这部分在chunk-vendor中占的比例不小
这部分实际上已是作了处理的了,具体操做参考ant-design-vue文档,按说明作没啥大坑,效果也符合预期。
部分组件是不常常用的,但却使用了Vue.use()全局引入了。这里去掉不经常使用和没用到的全局引入,改成页面内import()引入
这一部分,因为咱们在项目中只使用了几个Ant内置图标,不可能有530+KB。根据Ant文档的描述是因为其将ICON全量引入的关系致使的,说法是当前用法若是按需加载的话没法肯定使用者会不会在运行时改变icon,好比配置的ICON。
这个问题,在React版的Ant-Design是已是作了处理的了(写法上有所调整),但在Ant-Design-Vue-1.x中仍然没有官方解决方案。目前了解到的有两种方案
@ant-design/icons/lib/dist
指向项目中的antd-icon.js
,而后在antd-icon
中按需导出便可// alias配置
resolve: {
alias: {
'@ant-design/icons/lib/dist$': path.resolve(__dirname, './src/icons/antd-icon.js')
}
}
// src/icons/antd-icon.js
export { default as LoadingOutline } from '@ant-design/icons/lib/outline/LoadingOutline'
复制代码
- 注意项目中和Ant组件中使用的ICON都要导出
- 优化后从530K+到30K+
这两个包300K + 160K,加起来有460K,也是占的比较多的一项
这里使用内置的IgnorePlugin便可作到
// webpack plugins
plugins: [
// Ignore all locale files of moment.js
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
复制代码
moment-timezone包含了moment,项目中只有一处地方使用到,用来获取东八区的当前时间,moment是能够作到的,不须要额外引入moment-timezone
// old
moment.tz('Asia/Shanghai').format('YYYYMMDDHHmmss')
// new
moment.utcOffset(480).format('YYYYMMDDHHmmss')
复制代码
dayjs实现了moment的大多数功能,Api基本一致,压缩后体积却不到2k,是优秀的替代方案,且多数状况下,dayjs能够完美的替代moment(按需引入插件)
可是这里遇到了使用Ant-Design的第二个坑,Ant-Design实现Date-Picker时使用了moment,因此在项目中不使用moment也无论用,同样会打包进来....
这个问题,在React版的Ant-Design又已是作了处理的了,容许用户选择dayjs或moment。一样的,Ant-Design-Vue仍没有跟进...
所幸,dayjs做者提供了一个插件,能够将Ant-Design-Vue的moment替换成dayjs👍 虽然文档中只说Ant-Design-React能够用,但实际上在issue能够看到它也适用于antdV,不过须要调整一些配置
// vue.config.js
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin')
configureWebpack: {
...
plugins: [
new AntdDayjsWebpackPlugin({ preset: 'antdv3', replaceMoment: true })
],
}
复制代码
replaceMoment参数是用webpack alias来重定向moment到dayjs,因为项目中多处使用moment,这样作能够继续使用moment实际上调用的dayjs,实现无感知替换👍
- 替换成dayjs后,一些功能须要经过引入插件来保持一致,好比utc、updateLocale
- 替换成dayjs后,一些针对moment的优化须要改为dayjs或去掉
core-js实际上就是浏览器新API的polyfill,项目是PC端,因此主要是为了兼容IE...
项目中默认是useBuiltIns: 'entry'
将全部polyfill都引入了,致使包比较大。咱们可使用useBuiltIns: 'entry'
调整下策略,按需引入,项目中没使用到的API就不作polyfill处理了
// babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
'@babel/preset-env',
{
'useBuiltIns': 'usage', // entry,usage
'corejs': 3
}
]
],
plugins
}
复制代码
btw,这里更优的作法应该是使用动态polyfill,让服务器根据UA判断是否要返回polyfill
vue-cli3的默认优化是将全部npm依赖都打进chunk-vendor,但这种作法在依赖多的状况下致使chunk-vendor过大
optimization: isProd ? {
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity, // 默认为3,调整为容许无限入口资源
minSize: 20000, // 20K如下的依赖不作拆分
cacheGroups: {
vendors: {
// 拆分依赖,避免单文件过大拖慢页面展现
// 得益于HTTP2多路复用,不用太担忧资源请求太多的问题
name(module) {
// 拆包
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
// 进一步将Ant组件拆分出来,请根据状况来
// const packageName = module.context.match(/[\\/]node_modules[\\/](?:ant-design-vue[\\/]es[\\/])?(.*?)([\\/]|$)/)[1]
return `npm.${packageName.replace('@', '')}` // 部分服务器不容许URL带@
},
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
}
}
},
runtimeChunk: { name: entrypoint => `runtime-${entrypoint.name}` }
} : {}
复制代码
- Tips: vue inspect > output.js --mode production 能够查看最终配置
- 分包这块须要根据实际状况作对应处理,才能取得比较好的效果,总之多看文档多试就对了
SPA中一个很重要的提速手段就是路由懒加载,当打开页面时才去加载对应文件,咱们利用Vue的异步组件和webpack的代码分割(import()
)就能够轻松实现懒加载了。
但当路由过多时,请合理地用webpack的魔法注释对路由进行分组,太多的chunk会影响构建时的速度
{
path: 'register',
name: 'register',
component: () => import(/* webpackChunkName: "user" */ '@/views/user/register'),
}
复制代码
请只在生产时懒加载,不然路由多起来后,开发时的构建速度感人
HTTP2是HTTP协议的第二个版本,相较于HTTP1 速度更快、延迟更低,功能更多。 目前来看兼容性方面也算过得去,在国内有超过50%的覆盖率。
一般浏览器在传输时并发请求数是有限制的,超过限制的请求须要排队,以往咱们经过域名分片、资源合并来避开这一限制,而使用HTTP2协议后,其能够在一个TCP链接分帧处理多个请求(多路复用),不受此限制。(其他的头部压缩等等也带来了必定性能提高)
若是网站支持HTTPS,请一并开启HTTP2,成本低收益高,对于请求多的页面提高很大,尤为是在网速不佳时
// nginx.conf
listen 443 http2;
复制代码
nginx -s stop && nginx
复制代码
多路复用避开了资源并发限制,但资源太多的状况,也会形成浏览器性能损失(Chrome进程间通讯与资源数量相关)
Gzip压缩是一种强力压缩手段,针对文本文件时一般能减小2/3的体积。
HTTP协议中用头部字段Accept-Encoding
和 Content-Encoding
对「采用何种编码格式传输正文」进行了协定,请求头的Accept-Encoding
会列出客户端支持的编码格式。当响应头的 Content-Encoding
指定了gzip时,浏览器则会进行对应解压
通常浏览器都支持gzip,因此Accept-Encoding
也会自动带上gzip
,因此咱们须要让资源服务器在Content-Encoding
指定gzip,并返回gzip文件
#开启和关闭gzip模式
gzip on;
#gizp压缩起点,文件大于1k才进行压缩
gzip_min_length 1k;
# gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_comp_level 6;
# 进行压缩的文件类型。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
# nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,若是找不到则不进行压缩
gzip_static on
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
# 设置gzip压缩针对的HTTP协议版本
gzip_http_version 1.1;
复制代码
虽然上面配置后Nginx已经会在响应请求时进行压缩并返回Gzip了,可是压缩操做自己是会占用服务器的CPU和时间的,压缩等级越高开销越大,因此咱们一般会一并上传gzip文件,让服务器直接返回压缩后文件
// vue.config.js
const CompressionPlugin = require('compression-webpack-plugin')
// gzip压缩处理
chainWebpack: (config) => {
if(isProd) {
config.plugin('compression-webpack-plugin')
.use(new CompressionPlugin({
test: /\.js$|\.html$|\.css$/, // 匹配文件名
threshold: 10240, // 对超过10k的数据压缩
deleteOriginalAssets: false // 不删除源文件
}))
}
}
复制代码
- 插件的默认压缩等级是9,最高级的压缩
- 图片文件不建议使用gzip压缩,效果较差
<link>标签的rel属性的两个可选值。
Prefetch,预请求,是为了提示浏览器,用户将来的浏览有可能须要加载目标资源,因此浏览器有可能经过事先获取和缓存对应资源,优化用户体验。
Preload,预加载,表示用户十分有可能须要在当前浏览中加载目标资源,因此浏览器必须预先获取和缓存对应资源。
Prefetch、Preload能够在某些场景下能够有效优化用户体验。 举些场景
字体样式闪动
,而咱们使用Preload
提早加载字体后这种状况就好不少了,大图也是如此Vue-Cli3默认会使用preload-webpack-plugin对chunk资源作preload、prefetch处理,入口文件preload,路由chunk则是prefetch。
通常来讲不须要作特别处理,若是判断不须要或者须要调整在vue.config.js
中配置便可
- 理论上prefetch不会影响加载速度,但实际测试中,是有轻微影响的,不过这个见仁见智,我认为整体体验上仍是有所提高的,
- 相似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用preload
- 移动端流量访问时慎用
当应对一些弱网地区时,OSS + CDN无疑是很强力的提速手段。
海量,安全,低成本,高可靠的云存储服务。能够经过简单的REST接口,在任什么时候间、任何地点上传和下载数据,也可使用WEB页面对数据进行管理。
OSS的特色:
另外,OSS还提供一些方便的服务
这里推荐直接购买阿里家的OSS,OSS虽然也有传输加速服务,但对于静态热点文件的下载加速场景仍是须要CDN加速
CDN加速原理是把提供的域名做为源站,将源内容缓存到边缘节点。当客户读取数据时,会从最适合的节点(通常来讲就近获取)获取缓存文件,以提高下载速度。
因为没申请到资源,项目并无上OSS+CDN。但若是有条件仍是建议上,提高很大
首屏优化,在JS没解析执行前,让用户能看到Loading动画,减轻等待焦虑。一般会在index.html上写简单的CSS动画,直到Vue挂载后替换挂载节点的内容,但这种作法实测也会出现短暂的白屏,建议手动控制CSS动画关闭
首屏优化,APP内常见的加载时各部分灰色色块。针对骨架屏页的自动化生成,业界已有很多解决方案。
首屏之外的一些场景优化,更多相关内容好比图片懒加载、组件懒加载等 后续文章会作介绍
通常来讲,图片加载有两种方式,一种是自上而下扫描,一种则是原图的模糊展现,而后逐渐/加载完清晰。前者在网速差的时候用户体验较差,后者的渐进/交错式加载则能减轻用户的等待焦虑,带来更好的体验
浏览器自己支持这种图片的模糊到清晰的扫描加载方式,只须要将处理好资源便可
先加载小图,模糊化渲染,图片加载完成后替换为原图,最典型的例子就是Medium,模糊化能够用filter或者canvas处理
先加载全局通用loading图或者用CSS填充色块,图片加载完成后替换为原图。简单粗暴,在弱网条件下颇有用
- 几种方式能够同时搭配使用
- 渐进/交错格式图片会占用必定CPU和内存,酌情使用
弱网优化手段,用了懒加载后用户若是在弱网条件下点击下一个页面在下个页面加载完成前页面内容不可用,用户会理解为卡顿。 在VueRouter的路由守卫中处理便可
本文只介绍了首屏加载场景下的性能优化,实际上性能优化远不止这些内容,SPA的加载性能指标采集光靠Lighthouse、slow 3G模拟真的可信吗?除了加载场景外的其余方面呢?构建速度、操做流畅性...
性能优化影响的,不只是用户体验,还影响了转化率、搜索引擎排名,这些因素都会对最终的流量、销量等收入形成影响
来自Google的数据代表,一个有10条数据0.4秒能加载完的页面,变成30条数据0.9秒加载完以后,流量和广告收入降低90%。
Google Map 首页文件大小从100KB减少到70-80KB后,流量在第一周涨了10%,接下来的三周涨了25%。
亚马逊的数据代表:加载时间增长100毫秒,销量就降低1%。
立场不一样,看问题角度也会变化,好比对老板来讲最终目的实际上是搞钱hh,用户体验这些花里胡哨的,别人不必定懂。
因此若是必要,请在过程先后作好性能收益的数据监控和分析,在性能优化和产品指标之间创建正向联系,方便自上而下的推进技术方案的执行。这才是你说服上司或领导投入成本到性能优化上的重要依据
性能优化算是老生常谈的话题了,但部分人在面对怎么作性能优化的问题时,仅仅只是罗列出各类常见优化手段,更有深度的答案应该是遇到什么性能问题,针对这个问题围绕某些性能指标采起了什么手段,手段是否带来了其余问题,怎么权衡,最终达到了什么样的效果。