1. Vue 项目优化方式有哪些?
使用
计算属性
。 特点:可以被缓存使用
函数式组件
例如:对于某些组件,如果我们只是用来显示一些数据,不需要管理状态,监听数据等,那么就可以用函数式组件。
函数式组件 是
无状态的
,无实例的
,在初始化时不需要初始化状态,不需要创建实例,也不需要去处理生命周期等,相比有状态组件,会更加轻量,同时性能也更好。结合场景使用
v-show
和v-if
两者的作用: 都是用来控制某些组件或 DOM 的显示/隐藏
v-if
适用于在运行时很少改变条件,不需要频繁切换条件的场景v-show
适用于需要非常频繁切换条件的场景v-if
的部分被转换成了一个三元表达式,visible
为true
时,创建组件的vnode
,否则创建一个空 vnode
。在 patch 的时候,新旧节点不一样,就会移除旧的节点或创建新的节点,这样的话组件也会跟着创建/销毁。如果组件里有很多 DOM,或者要执行很多初始化/销毁逻辑,那么随着visible
的切换,势必会浪费掉很多性能v-show
被编译成了directives
,它其实是通过切换元素的display 属性
来控制的,和v-if
相比,不需要在patch 阶段
创建/移除节点,只是根据v-show
上绑定的值来控制DOM 元素
的style.display
属性,在频繁切换的场景下就可以节省很多性能
注意: 如果初始值是
false
时,v-if
并不会创建隐藏的节点,但是v-show
会创建,并通过设置style.display='none'
来隐藏,虽然外表看上去这个 DOM 都是被隐藏的,但是v-show
已经完整的走了一遍创建的流程,造成了性能的浪费总结:
v-if
的优势体现在初始化
时,v-show
体现在更新
时(初始化性能压力大)使用
keep-alive
keep-alive 的作用 就是将它包裹的组件在第一次渲染后就缓存起来,下次需要时就直接从缓存里面取,避免了不必要的性能浪费
避免
v-for
和v-if
同时使用Vue2 中,
v-for
的优先级比v-if
高;Vue3 中,v-if
的优先级比v-for
高总结:使用计算属性代替
给
v-for
添加key
, 并且不要将index
或者随机数
作为key
延迟渲染
延迟渲染 就是
分批渲染
,假设我们某个页面里有一些组件在初始化时需要执行复杂的逻辑,这将会占用很长时间,导致帧数下降、卡顿,其实可以使用分批渲染的方式来进行优化,就是先渲染一部分,再渲染另一部分<template> <Heavy v-if="defer(1)" /> </template> <script> export default { data() { return { displayPriority: 0 } }, mounted() { this.runDisplayPriority() }, methods: { runDisplayPriority() { const step = () => { requestAnimationFrame(() => { this.displayPriority++ if (this.displayPriority < 10) { step() } }) } step() }, defer(priority) { return this.displayPriority >= priority } } } </script>
原理:主要是维护
displayPriority
变量,通过requestAnimationFrame
在每一帧渲染时自增,然后我们就可以在组件上通过v-if="defer(n)"
使displayPriority
增加到某一值时再渲染,这样就可以避免js 执行
时间过长导致的卡顿问题了使用
非响应式数据
在
Vue 组件
初始化数据时,会递归遍历在data
中定义的每一条数据,通过Object.defineProperty
将数据改成响应式,这就意味着如果 data 中的数据量很大的话,在初始化时将会使用很长的时间去执行Object.defineProperty
, 也就会带来性能问题,这个时候我们可以强制使数据变为非响应式,从而节省时间解决方法:
Object.freeze()
扩展:为什么
Object.freeze()
会有这样的效果呢?对某一对象使用
Object.freeze()
后,将不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值而
Vue
在将数据改造成响应式
之前有个判断:export function observe(value, asRootData) { // ...省略其他逻辑 if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } // ...省略其他逻辑 }
这个判断条件中有一个
Object.isExtensible(value)
,这个方法是判断一个对象是否是可扩展
的,由于我们使用了Object.freeze()
,这里肯定就返回了false
,所以就跳过了下面数据劫持的过程,没有了收集依赖
的过程,自然也就节省了性能。问题:数据都不是响应式的,可以只对这种数据的某一层使用
Object.freeze()
,同时配合使用上文中的延迟渲染
、函数式组件
等,可以极大提升性能。computed
、watch
、methods
区分使用场景computed:
一个数据受多个数据影响的。watch
: 一个数据影响多个数据的。当数据变化时,需要执行异步或开销较大的操作时。如果数据变化时请求一个接口。methods
: 希望数据是实时更新,不需要缓存。
防抖和节流
- 防抖:触发事件后规定时间内事件只会执行一次。简单来说就是防止手抖,短时间操作了好多次。
- 节流:事件在规定时间内只执行一次。
- 应用场景: 节流不管事件有没有触发还是频繁触发,在规定时间内一定会只执行一次事件,而防抖是在规定时间内事件被触发,且是最后一次被触发才执行一次事件。
图片大小优化和懒加载
- 图片大小的优化,可以用
image-webpack-loader
进行压缩图片 - 图片懒加载,可以用
vue-lazyload
插件实现 问题:vue-lazyload
如何实现?考查Vue 插件、指令
- 图片大小的优化,可以用
利用挂载节点会被替换的特性优化白屏问题
我们可以在
<div id="app"></div>
里添加首屏的静态页面。等真正的首屏加载出来后就会把<div id="app"></div>
这块结构都替换掉,给人一种视觉上的误差,就不会产生白屏组件库按需引入 如
element UI
库,用babel-plugin-component
插件实现按需引入在根目录下
.babelrc.js
文件中按如下配置:{ "presets": [["es2015", { "modules": false }]], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ] }
其中
libraryName
为组件库的名称,styleLibraryName
为组件库打包后样式存放的文件夹名称。问题:
babel-plugin-component
是如何做到按需加载的?其实 b
abel-plugin-component
插件是 element 用babel-plugin-import
插件改造后特定给 element UI 使用。一般的组件库还是babel-plugin-import
插件实现按需引入。打包优化:
webpack-bundle-analyzer
可以帮助你可视化的分析打包后的各个资源的大小利用
import()
异步引入组件实现按需引入路由懒加载,所谓的懒加载就是用
import()
异步引入组件component: () =>import('views/home.vue'), component: resolve =>require(['views/home.vue'],resolve)
resolve 方式打包会把所有组件的代码都打包在一个 js 文件。预期应该是每个组件的代码都被打包成对应的 js 文件,加载组件时会对应加载 js 文件,这才是懒加载。
- 可以使用
webpackChunkName: chunk
文件的名称, [request]表示实际解析的文件名
function load(component) { return () => import(/* webpackChunkName: "[request]" */ `views/${component}`) }
- 使用懒加载后,浏览器的 network 就会出现
Purpose: prefetch
,只是预取(prefetch
)一下,没有返回内容的。目的是告诉浏览器,空闲的时候给我加载这个js 文件
。直到真正加载这个路由组件时,这个js 文件
再次被加载
扩展:
preload
和prefetch
的区别preload
(预加载):可以强制浏览器在不阻塞document
的onload
事件的情况下请求资源prefetch
(懒加载): 告诉浏览器这个资源将来可能需要,但是什么时间加载这个资源是由浏览器来决定的- 可以使用
利用
externals
提取第三方依赖并用CDN
引入在
Webpack
中的externals
配置选项,可避免将第三方依赖打包,而是在项目运行时从外部获取第三方依赖。利用
SplitChunks
插件提取公共js 代码
和分割 js 代码
压缩
图片
、HTML
、CSS
、JS
等静态资源
项目部署的优化
识别
gzip
压缩是否开启只要看响应头部(Response headers)中 有没有
Content-Encoding: gzip
这个属性即可,有代表有开启gzip
压缩。在 Nginx 上开启 gzip 压缩
在
nginx/conf/nginx.conf
中配置http { // on | off ,默认为off,on为开启gzip,off为关闭gzip gzip on; // number,压缩起点,文件大于多少才进行压缩,单位默认为字节,也可用k表示千字节 gzip_min_length 1k; // 压缩级别,1-9,数字越大,压缩后的大小越小,也越占用CPU,花费时间越长 gzip_comp_level 5; // 需要进行压缩的文件类型。类型去Response headers中看Content-Type属性 gzip_types application/javascript image/png image/gif image/jpeg text/css text/plain; // number size,设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流(例如 4 4k代表以4k为单位,按照原始数据大小以4k为单位的4倍申请内存。如原始数据大小为17K,则申请 (17/4)*4 = 17k内存) gzip_buffers 4 4k; // 设置gzip压缩针对的HTTP协议版本以上 gzip_http_version 1.1; // on | off,是否在http header中添加Vary:Accept-Encoding,on表示添加。Vary:Accept-Encoding告诉代理服务器缓存两种版本的资源:压缩和非压缩,避免一个浏览器不支持压缩资源,而先请求了服务器,服务器缓存了非压缩的资源,然后一个浏览器支持压缩资源,再去请求了服务器,结果得到非压缩资源,但是又去解压它,结果会出错。所以建议设置为on gzip_vary on; }
在
Webpack
上开启gzip
压缩 利用CompressionWebpack
插件来实现gzip
压缩CompressionWebpack
参数详解:- test:String|RegExp|Array<String|RegExp>,资源的名称符合条件的才会被压缩,默认为 undefined,即全部符合,例如只要压缩 js 文件
plugins: [ new CompressionPlugin({ test: /\.js(\?.*)?$/i, }) ],
- include:String|RegExp|Array<String|RegExp>,资源的名称符合条件的才会被压缩,默认为 undefined,是在 test 参数的范围内在进行筛选,满足 test 参数的条件,且满足 include 参数的条件的资源才会被压缩
- exclude:String|RegExp|Array<String|RegExp>,压缩时排除资源的名称符合条件的资源,默认为 undefined,是在 test 参数的范围内在进行排除,满足 test 参数的条件,不满足 exclude 参数的条件的资源才会被压缩
- algorithm:压缩算法/功能,默认 gzip,一般不做更改
- compressionOptions,对 algorithm 参数所选用的压缩功能的参数设置,一般用来设置压缩级别,1-9,数字越大,压缩后的大小越小,也越占用 CPU,花费时间也越长
plugins: [ new CompressionPlugin({ compressionOptions: { level: 1 }, }) ],
- threshold:Number,设置被压缩资源的最小大小,单位为字节。默认为 0
- minRatio:Number,设置压缩比率,压缩比率 = 压缩后的资源的大小/压缩后的资源,小于压缩比率的资源才会被压缩。和 threshold 参数是‘与’的关系
- filename:类型:String|Function,设置压缩资源后的名称,默认值:[path].gz[query],[file]被替换为原始资产文件名。 [path]替换为原始资产的路径。 [dir]替换为原始资产的目录。 [name]被替换为原始资产的文件名。 [ext]替换为原始资产的扩展名。 [query]被查询替换
new CompressionPlugin({ filename(info) { console.log(info) return `${info.path}.gz${info.query}` }, })
Nginx
和Webpack
压缩的区别不管 Nginx 还是 Webpack 压缩,在 Nginx 中都要开启 gzip 压缩,不然浏览器加载还是未压缩的资源。
还可以在 Nginx 加上 gzip_static on;的配置。gzip_static 启用后, 浏览器请求资源时,Nginx 会先检查是否存该资源名称且后缀为.gz 的文件,如果有则直接返回该 gz 文件内容,可以避免 Nginx 对该资源再进行 gzip 压缩,浪费服务器的 CPU。
用 Nginx 压缩会占用服务器的 CPU,浏览器每次请求资源,Nginx 会对该资源实时压缩,压缩完毕后才会返回该资源,如果资源很大的话,还是压缩级别设置很高,都会导致返回资源的时间过长,造成不好的用户体验。
用 Webpack 会使打包时间变长。但是用 CompressionPlugin 插件压缩,会有缓存,可以相对减少打包时间。
建议 Nginx 和 Webpack 压缩都开启压缩,且在 Nginx 加上 gzip_static on;的配置,减少服务器的 CPU 的使用,当然还是要根据项目的情况实际选择。
nginx
配置vue
项目缓存vue
的所有资源修改后打包出来的名称都会改变,所以可以使用强缓存,对css
、js
、png
、ttf
、jpg
等location ~* \.(css|js|png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)$ { access_log off; add_header Cache-Control max-age=604800; }
html
文件因为名称不会改变,所以使用协商缓存
,html 文件有改动就会立即更新,max-age=no-cache
代表进入协商缓存,文件改动会自动更新,不改动会返回 304location ~* \.(html)$ { access_log off; add_header Cache-Control max-age=no-cache; }
2. 常见的性能优化手段?
- 减少 http 请求
- 使用 http2
- 静态资源使用 CDN
- 将 CSS 放在文件头部,JavaScript 文件放在底部
- 使用字体图标 iconfont 代替图片图标
- 设置缓存,强缓存,协商缓存
- 压缩文件,css(MiniCssExtractPlugin),js(UglifyPlugin),html(html-webpack-plugin)文件压缩,清除无用的代码,tree-shaking(需要 es6 的 import 才支持),gzip 压缩(compression-webpack-plugin)
- splitChunks 分包配置,optimization.splitChunks 是基于 SplitChunksPlugin 插件实现的
- 图片优化、图片压缩
- webpack 按需加载代码,hash,contenthash
3. 骨架屏
4. 减少首屏渲染时间
5. 前端性能监控
前端监控:它指的是通过一定的手段来获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,为产品优化指明方向,为用户提供更加精确、完善的服务。
前端监控
一般分为三大类:
- 数据监控(监控用户行为)
- PV/UV:PV 即页面浏览器或点击率;UV 即访问某个站点或点击某条新闻的不同 IP 地址的人数
- 用户在每一个页面的停留时间
- 用户通过什么入口来访问该网页
- 用户在相应的页面中触发的行为...等
- 性能监控(监控页面性能)
- 不同用户,不同机型和不同系统下的首屏加载时间
- 白屏时间
- http 等请求的响应时间
- 静态资源整体下载时间
- 页面渲染时间...等
- 异常监控(监控产品、系统异常)
- JavaScript 的异常监控
- 样式丢失的异常监控...等
7. 前端性能埋点
埋点的三种方法:
手动埋点
(代码埋点):在需要埋点的业务逻辑功能位置调用接口,上报埋点数据(像友盟、百度统计、神策) 缺点:项目工程量大,需要埋点的位置太多可视化埋点
(通过可视化交互的手段,代替上述的代码埋点。将业务代码和埋点代码分离) 缺点:埋点的控件有限,不能手动定制无埋点
(前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据) 优点:前端只要一次加载埋点脚本;缺点:流量和采集的数据过于庞大,服务器性能压力大
埋点上报的方式:
- 图片(优先考虑 GIF) 优点:防止跨域、防止阻塞页面加载,影响用户体验、相比 PNG/JPG,GIF 的体积最小
大多采用的是
1*1
像素的透明GIF
来上报- Beacon 用于将数据异步发送到服务器。navigator.sendBeacon(url, data);