思维导图
一.npm run build 缩小打包的体积
首先来看我未作任何处理的一个打包
这个包很大,里面有很多文件。你会发现里面有很多.map结尾的文件,占据了非常大的空间,下面我们一步一步来:
1.productionSourceMap
Type: boolean
Default: true
用途:
设置生产环境的 source map 开启与关闭。
用法
module.exports = {
publicPath: './', // 基本路径
outputDir: 'dist', // 输出文件目录
assetsDir: './assets',
indexPath: 'index.html',
filenameHashing: true, // 生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存
lintOnSave: false, // eslint-loader 是否在保存的时候检查
productionSourceMap: true, // 生产环境是否生成 sourceMap 文件
}
source map 直译过来就是资源地图。所以,source map的作用就是定位。source map定位的时浏览器控制台输出语句在项目文件的位置。
好的,加下来我们把productionSourceMap改为false,看看效果:
发现.map文件都没有了,进入下一步:
webpack-bundle-analyzer
这是一款用户分析打包大小的插件,使用如下:
cnpm install webpack-bundle-analyzer --s-d
在vue-cli3.0项目中,新建vue.config.js中
module.exports = {
productionSourceMap: false,
chainWebpack:config=>{
config.plugin('webpack-bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
}
}
在vue-cli2.x项目中,在build/webpack.prod.config.js中的module.export - webpackConfig前面加上:
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
执行:
npm run build --report
就可以看到:
fast 3G的情况下加载了17s,可以看到vendor.js这个文件有1.84m,里面echarts和elementui这两个库占了大头。address.json这个是地理位置的json,最好是可以改成后台获取,以减小打包文件体积,这里暂时不表。先来看看怎么处理那两个大头。
有两个方向:
1.按需加载
本来直接在Main.js中:
import echarts from 'echarts'
Vue.prototype.$echarts = echarts
现在改为在echarts组件里引入
var echarts = require('echarts/lib/echarts');
require('echarts/lib/chart/line') // 按需导入折线组件
require('echarts/lib/component/tooltip') // 提示组件
require('echarts/lib/component/legend') // 图例组件
2.使用cdn托管
大型Web应用对速度的追求并没有止步于仅仅利用浏览器缓存,因为浏览器缓存始终只是为了提升二次访问的速度,对于首次访问的加速,我们需要从网络层面进行优化,最常见的手段就是CDN(Content Delivery Network,内容分发网络)加速。通过将静态资源缓存到离用户很近的相同网络运营商的CDN节点上,不但能提升用户的访问速度,还能节省服务器的带宽消耗,降低负载。不同地区的用户会访问到离自己最近的相同网络线路上的CDN节点,当请求达到CDN节点后,节点会判断自己的内容缓存是否有效,如果有效,则立即响应缓存内容给用户,从而加快响应速度。如果CDN节点的缓存失效,它会根据服务配置去我们的内容源服务器获取最新的资源响应给用户,并将内容缓存下来以便响应给后续访问的用户。因此,一个地区内只要有一个用户先加载资源,在CDN中建立了缓存,该地区的其他后续用户都能因此而受益。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"/>
<meta http-equiv="pragram" content="no-cache">
<meta http-equiv="cache-control" content="no-cache, must-revalidate">
<meta http-equiv="expires" content="0">
<title>chitic-sems-app</title>
<link rel="stylesheet" href="https://unpkg.com/mint-ui/lib/style.css"></head>
<% for (var i in htmlWebpackPlugin.options.css) { %>
<link href="<%= htmlWebpackPlugin.options.css[i] %>" rel="stylesheet">
<% } %>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<% for (var i in htmlWebpackPlugin.options.js) { %>
<script src="<%= htmlWebpackPlugin.options.js[i] %>"></script>
<% } %>
</body>
</html>
这里有个坑:
1.不要把任何页面内容写到 body、head 外面。
2.app.js 插入到了你写的 script 的前面,获取不到 Vue。
JS部分
vue-cli2.X项目
首先在config/index.js文件中:
在dev和build内部加入:
cdn: {
css:[],
js: [
'https://cdn.staticfile.org/vue/2.6.10/vue.min.js',
'https://cdn.staticfile.org/vuex/3.0.1/vuex.min.js',
'https://cdn.staticfile.org/vue-router/3.0.3/vue-router.min.js',
'https://cdn.bootcss.com/element-ui/2.13.2/index.js',
'https://cdn.bootcss.com/echarts/4.2.1/echarts.simple.min.js'
]
}
dev用于开发环境,build用于开发环境的资源请求。
然后在webpack.base.conf.js的module.export{}内部加入
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'element-ui':'ELEMENT',
'echarts':'echarts'
}
externals的目的就是将不怎么需要更新的第三方库脱离webpack打包,不被打入bundle中,从而减少打包时间,但又不影响运用第三方库的方式
下一步在webpack.dev.conf.js中利用HtmlWebpackPlugin注入资源
new HtmlWebpackPlugin(Object.assign(
{
filename: 'index.html',
template: 'index.html',
inject: true
},
config.dev.cdn
)),
此时你开发环境下已经引入了这些资源了。
下一步在webpack.prod.conf.js中利用HtmlWebpackPlugin注入资源
new HtmlWebpackPlugin(Object.assign({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
},
config.build.cdn)),
大功告成,让我们运行一下npm run build --report
当把address.json也改成接口之后,我们再来看看打包的大小
vue-cli3.0项目
新建vue.config.js
// 代码压缩
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
// 是否为生产环境
const isProduction = process.env.NODE_ENV !== 'development'
// 本地环境是否需要使用cdn
const devNeedCdn = true
// cdn链接
const cdn = {
// cdn:模块名称和模块作用域命名(对应window里面挂载的变量名称)
externals: {
vue: 'Vue',
vuex: 'Vuex',
'vue-router': 'VueRouter',
'element-ui':'ELEMENT'
},
// cdn的css链接
css: [],
// cdn的js链接
js: [
'https://cdn.staticfile.org/vue/2.6.10/vue.min.js',
'https://cdn.staticfile.org/vuex/3.0.1/vuex.min.js',
'https://cdn.staticfile.org/vue-router/3.0.3/vue-router.min.js',
'https://cdn.bootcss.com/element-ui/2.13.2/index.js'
]
}
let path = require('path')
module.exports = {
productionSourceMap: false, //优化打包体积
publicPath: './',
lintOnSave: false,
chainWebpack:config =>{
// ============注入cdn start============
config.plugin('html').tap(args => {
// 生产环境或本地需要cdn时,才注入cdn
if (isProduction || devNeedCdn) args[0].cdn = cdn
return args
})
config.plugin('webpack-bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
// ============注入cdn start============
},
configureWebpack: config => {
// 用cdn方式引入,则构建时要忽略相关资源
if (isProduction || devNeedCdn) config.externals = cdn.externals
// 生产环境相关配置
if (isProduction) {
// 代码压缩
config.plugins.push(
new UglifyJsPlugin({
uglifyOptions: {
//生产环境自动删除console
compress: {
// warnings: false, // 若打包错误,则注释这行
drop_debugger: true,
drop_console: true,
pure_funcs: ['console.log']
}
},
sourceMap: false,
parallel: true
})
)
}
// 生产环境相关配置
}
}
来 运行打包
可以看到原来1.84m的文件,现在只剩下了250k,让我们情理缓存之后再来看一下加载速度:
可以看到从17s到了9s 缩短了一半,接下来还有什么办法呢,我们继续下一步
Gzip压缩
gzip压缩可以提高2-3倍的速度,非常棒。让我们来操作一下
选择的项目比较老,来讲一下
vue-cli2.X的
下载compression-webpack-plugin这个plugin
在你的webpack.prod.conf.js文件里添加以下代码:(与webpackConfig平级)
//开启gzip压缩
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
再将config文件夹下的index.js中的productionGzip改为true即可,如果报错
说明你的plugin版本太高了,降低版本即可
npm install --save-dev compression-webpack-plugin@1.1.12
vue-cli3.0的
在vue.config.js内加入
// gzip压缩
const productionGzipExtensions = ['html', 'js', 'css']
config.plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' + productionGzipExtensions.join('|') + ')$'
),
threshold: 10240, // 只有大小大于该值的资源会被处理 10240
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
})
)
}
此时我们来执行打包之后就可以看到
超过10k的文件都出现了.gz结尾的压缩包,体积只有原来的1/3-1/2。如果资源请求这些压缩包的话,加载速度还会提升一个档次。当然光是前端这样做还不够,还需要配置nginx。这里还有一个小技巧,在webpack.prod.conf.js的UglifyJsPlugin中添加一下一句话,可以去掉生产环境里的console.log
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false,
// =====以下是新增的=====
drop_console: true, // 删除页面中的 console.log
pure_funcs: ['console.log']
// =====以上是新增的=====
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
下面我们来讲讲nginx的配置
在nginx.conf内添加如下代码:
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml
text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
整体配置如下
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
upstream njsolar {
server localhost:9000; #Apache
}
server {
listen 8083;
server_name localhost;
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/local/deploy/njsolar-ui/dist;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
}
}
此时我们启动nginx:./nginx.exe,我们会发现:
我们成功请求到了gzip文件,vender文件从最初的1.8M缩减到了现在的75kb,请求时间也从17s缩减到了7s,如果从fast3G切换成online,基本上加载时间就在一秒左右,取得巨大进步。
如果还要再进一步的话,那就可以用SSR和骨架屏的方案,比较复杂,这里不加赘述。vue可以用nuxt.js框架开发。
首屏加载与打包的优化讲完了,接下来我们来讲一讲
Vue代码优化
1.路由懒加载
未用懒加载,vue中路由代码如下
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component:HelloWorld
}
]
})
vue异步组件实现懒加载
方法如下:component:resolve=>(require(['需要加载的路由的地址']),resolve)
import Vue from 'vue'
import Router from 'vue-router'
/* 此处省去之前导入的HelloWorld模块 */
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: resolve=>(require(["@/components/HelloWorld"],resolve))
}
]
})
ES 提出的import方法,(------最常用------)
方法如下:const HelloWorld = ()=>import('需要加载的模块地址')(不加 { } ,表示直接return)
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const HelloWorld = ()=>import("@/components/HelloWorld")
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component:HelloWorld
}
]
})
2.组件懒加载
原来组件中写法
<template>
<div class="hello">
<One-com></One-com>
1111
</div>
</template>
<script>
import One from './one'
export default {
components:{
"One-com":One
},
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
组件懒加载写法:
<template>
<div class="hello">
<One-com></One-com>
1111
</div>
</template>
<script>
export default {
components:{
"One-com":resolve=>(['./one'],resolve)
},
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
3.http连接优化
HTTP协议定义Web客户端如何从服务器请求web页面,以及服务器如何把web页面传送给客户端,浏览器请求获取数据通过http连接完成,因此,优化http连接优化是web项目优化的重要方向之一。Web项目的由浏览器呈现,下面我们来看看浏览器对请求的处理过程
附上几份缓存相关的教程:
https://www.cnblogs.com/fangsmile/p/13072940.html
https://blog.csdn.net/lianxin19900610/article/details/82417233
https://mp.weixin.qq.com/s/FVNlCZhOpJk5gUCnhsmSzQ
3.1 减少HTTP请求
3.1.1 合并CSS、js、图片。将css、js、图片合并,可以有效减少http请求数;
3.1.2 合理规划API,将可合并的接口请求合并。浏览器对并发的http请求数量有限制,即使API响应很快,在多个接口并发请求时,仍会在浏览器中造成不同程度的卡顿;
3.2合理使用缓存
合理设置HTTP缓存。从上图浏览器请求处理流程图中可以看出,恰当的缓存设置可以大大减少HTTP请求,节省带宽。如很少变化的资源(html、css、js、图片等)通过 HTTP Header中的cache-control和Expires可设定浏览器缓存,变化不频繁又可能变的资源使用Last-Modifed来做请求验证。
3.3使用字体图标,少用图片
使用字体图标的优点:
3.3.1轻量:一个图标字体比一系列的图像(特别是在Retina屏中使用双倍图像)要小。一旦图标字体加载了,图标就会马上渲染出来,不需要下载一个图像。可以减少HTTP请求,还可以配合HTML5离线存储做性能优化;
3.3.2灵活性:图标字体可以用过font-size属性设置其任何大小,还可以加各种文字效果,包括颜色、Hover状态、透明度、阴影和翻转等效果。可以在任何背景下显示。
3.3.3兼容性:网页字体支持所有现代浏览器,包括IE低版本。
3.4图片懒加载
在实际的项目开发中,我们通常会遇见这样的场景:一个页面有很多图片,而首屏出现的图片大概就一两张,那么我们还要一次性把所有图片都加载出来吗?显然这是愚蠢的,不仅影响页面渲染速度,还浪费带宽。这也就是们通常所说的首屏加载,技术上现实其中要用的技术就是图片懒加载--到可视区域再加载。
4.其他小技巧
4.1尽量提取公共方法和公共组件。
4.1.1提取公共组件,尽量与业务隔离,如筛选条件等;
4.1.2提取公共方法,放在util.js中,如表单校验封装;
4.1.3提取在项目中出现较多的功能模块,如弹窗详情等;
4.2 v-for和v-if
在vue中v-for和v-if不要放在同一个元素上使用。由于 v-for 和 v-if 放在同一个元素上使用会带来一些性能上的影响,而且v-for时要加上key,便于虚拟dom的遍历与渲染,当旧值和新值去对比的时候,可以更快的定位到diff
4.3 Keep-alive
一些不太需要刷新的页面,内容又比较复杂的,建议用Keep-alive。Keep-alive会新增两个生命钩子函数。
4.4 created()和mouted()
一些异步请求尽量放在mouted里面,这样不会导致因为请求过久让页面长时间白屏。
4.5 treesharking
webpack会自动在生产环境下(mode:production)进行treesharking,前提是依赖必须通过es6的方式引入:import.
4.6 避免回流和重绘
回流(Reflow)
当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的事件:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的 DOM 元素
- 激活 CSS 伪类(例如:hover)
- 查询某些属性或调用某些方法
- 一些常用且会导致回流的属性和方法
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、
getBoundingClientRect()、scrollTo()
重绘(Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
如何避免
CSS
1.避免使用 table 布局。
2.尽可能在 DOM 树的最末端改变 class。
3.避免设置多层内联样式。
4.将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。
5.避免使用 CSS 表达式(例如:calc())。
Javascript
1.避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
// 优化前
const el = document.getElementById('test');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 优化后,一次性修改样式,这样可以将三次重排减少到一次重排
const el = document.getElementById('test');
el.style.cssText += '; border-left: 1px ;border-right: 2px; padding: 5px;'
2.避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
3.也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
4.避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
5.对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
4.7 防抖与节流
防抖debounce
监听一个input输入框的keyup,但是不能一松开键盘就触发事件,因为可能会频繁调用接口,应该松开若干毫秒后不再按下,才开始调用接口
const input1 = document.getElementById('input')
var timer = null
input1.addEventListener('keyup',function(){
//假如输入123,首先输入1的时候,time是null,触发定时器,
// 500毫秒之后打印,但是在500毫秒之内再次输入了2,触发keyup,此时time!=null
// 就清空了原来应该打印1的定时器,重新执行一个新的定时器打印12,关键在于清空原来的定时器
if(timer){
clearTimeout(timer)
}
timer = setTimeout(() => {
console.log(input1.value)
timer = null
}, 500);
})
// 封装一个方法
function debounce(fn,delay = 500){
// timer在闭包中
let timer =null
return function(){
if(timer){
clearTimeout(timer)
}
timer = setTimeout(() => {
fn()
// fn.apply(this,arguments) 用这种更完美
timer = null
}, delay);
}
}
input1.addEventListener('keyup',debounce(()=>{
console.log(input1.value)
},1000))
节流throttle
拖拽一个元素时,要随时拿到该元素被拖拽的位置,直接用drag事件,则会频繁触发,很容易导致卡顿。 节流:无论拖拽速度多快,都会每隔100ms触发一次
const div1 = document.getElementById('div1')
var timer = null
div1.addEventListener('drag',function(e){
//存在timer则说明前一次还没执行完,必须前一次执行完,才能执行下一次操作,确保规定时间只执行一次,
// 和防抖的区别在于,防抖是清空原来执行新的,节流是执行原来的,正好相反
if(timer){
return
}
timer = setTimeout(() => {
console.log(e.offsetX,e.offsetY)
timer = null
}, 500);
})
// 封装一个方法
function throttle(fn,delay = 500){
// timer在闭包中
let timer =null
return function(){
if(timer){
return
}
timer = setTimeout(() => {
console.log(this) //this是div1,箭头函数承接上文,就是return的方法
fn.apply(this,arguments) //只是为了绑定事件的参数,fn.apply({},arguments)也可以起到效果
timer = null
}, delay);
}
}
div1.addEventListener('drag',throttle((e)=>{
console.log(this) //this是window 箭头函数承接上文,就是window
console.log(e.offsetX,e.offsetY)
},1000))
div1.addEventListener('drag',throttle(function(e){
console.log(this) //this是div
console.log(e.offsetX,e.offsetY)
},1000))
性能监控
window.performance.timing
以下给出统计页面性能指标的方法。
let times = {};
let t = window.performance.timing;
// 优先使用 navigation v2 https://www.w3.org/TR/navigation-timing-2/
if (typeof win.PerformanceNavigationTiming === 'function') {
try {
var nt2Timing = performance.getEntriesByType('navigation')[0]
if (nt2Timing) {
t = nt2Timing
}
} catch (err) {
}
}
//重定向时间
times.redirectTime = t.redirectEnd - t.redirectStart;
//dns 查询耗时
times.dnsTime = t.domainLookupEnd - t.domainLookupStart;
//TTFB 读取页面第一个字节的时间
times.ttfbTime = t.responseStart - t.navigationStart;
//DNS 缓存时间
times.appcacheTime = t.domainLookupStart - t.fetchStart;
//卸载页面的时间
times.unloadTime = t.unloadEventEnd - t.unloadEventStart;
//tcp 连接耗时
times.tcpTime = t.connectEnd - t.connectStart;
//request 请求耗时
times.reqTime = t.responseEnd - t.responseStart;
//解析 dom 树耗时
times.analysisTime = t.domComplete - t.domInteractive;
//白屏时间
times.blankTime = (t.domInteractive || t.domLoading) - t.fetchStart;
//domReadyTime
times.domReadyTime = t.domContentLoadedEventEnd - t.fetchStart;
当然现在如果要做性能监控的话,一般都不用自己写,git上有大把的开源项目给你选择。
这篇性能优化的文章写的也很不错哦:https://segmentfault.com/a/1190000019185648