Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。
v-if=false时不渲染DOM,v-show会预渲染DOM,v-if 在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建
computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch:更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
Vue组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
也就是说,在js内使用addEventListener等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露
created() {
addEventListener('touchmove', this.touchmove, false)
},
beforeDestroy() {
removeEventListener('touchmove', this.touchmove, false)
}
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze() (冻结) 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。或者可以用 Object.seal() (密封),具体区别自己去查吧
export default {
data: () => ({
users: {
}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
}
另外:
单纯大量数据渲染的列表:
如果我们一次性渲染刷新几万条数据,页面会卡顿,因此只能分批渲染,既然知道原理我们就可以使用setInterval和setTimeout、requestAnimationFrame来实现定时分批渲染,实现每16 ms 刷新一次
DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能
const render = () => {
}
const id = requestAnimationFrame(render)
// 60HZ 刷新频率,每次刷新间隔执行一次回调函数,不会丢帧,不会卡顿
// 不会重绘或回流
cancelAnimationFrame(id)
// 取消循环
function refresh(total, onceCount) {
//total -> 渲染数据总数 onceCount -> 一次渲染条数
let count = 0, //初始渲染次数值
loopCount = total / onceCount //渲染次数
function refreshAnimation() {
/*
* 在此处渲染数据
*/
if (count < loopCount) {
count++
requestAnimationFrame(refreshAnimation)
}
}
requestAnimationFrame(refreshAnimation)
}
写个例子::
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>大量数据加载和花样打印</title>
</head>
<body>
<div class="box">
box
<div id="create"></div>
<ul id="ulBox"></ul>
</div>
</body>
<script>
(function () {
const ul = document.getElementById('ulBox')
if(!ul){
return
}
const total = 100000;
const once = 20;
const number = total / once;
var areadyNum = 0;
const array = new Array(100000) //存放将要渲染的列表数据
for (var i = 0; i < array.length; i++){
array[i] = i;
}
function addItem() {
// 创建一个虚拟节点占位
const fragment = document.createDocumentFragment();
for( var i = 0; i < once; i++){
const li = document.createElement("li")
li.innerText = array[areadyNum * once + i + 1]
fragment.appendChild(li);
}
ul.appendChild(fragment)
continueDo();
areadyNum++;
}
function continueDo () {
if(areadyNum < number){
window.requestAnimationFrame(addItem);
}
}
continueDo();
// 点击列表项
ul.addEventListener('click', function(e) {
if(e.target.tagName === "LI"){
alert(e.target.innerText)
}
})
})()
</script>
</html>
在index.html中引入cdn资源
...
<body>
<div id="app">
</div>
<!-- built files will be auto injected -->
<script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.bootcss.com/vue-resource/1.5.1/vue-resource.min.js"></script>
</body>
...
修改 build/webpack.base.conf.js
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
externals:{
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex':'Vuex',
'vue-resource': 'VueResource'
},
...
}
修改src/main.js src/router/index.js 注释掉import引入的vue,vue-resource
// import Vue from 'vue'
// import VueResource from 'vue-resource'
// Vue.use(VueResource)
vue-cli3.0 的话
vue.config.js
module.exports = {
baseUrl: process.env.NODE_ENV === "production" ? "./" : "/",
outputDir: process.env.outputDir,
configureWebpack: {
externals: {
vue: "Vue",
vuex: "Vuex",
"vue-router": "VueRouter",
"element-ui": "ELEMENT"
}
}
};
将长时间不改变的第三方库或者静态资源设置强缓存,max-age;
其他静态资源设置协商缓存;
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存),如果对缓存机制还不是了解很清楚的,可以参考作者写的关于 HTTP 缓存的文章《彻底弄懂HTTP缓存机制及原理》,这里不再赘述。
对于现代浏览器来说,可以给link标签添加preload,prefetch,dns-prefetch属性
preload:预加载相关资源,preload的资源会和页面需要的静态资源并行加载
prefetch:空闲加载,其他资源加载完空间时间加载;
dns-prefetch:让浏览器提前对域名进行解析,减少DNS查找的开销,如果你的静态资源和后端接口不是同一个服务器的话,可以将考虑你后端的域名放入link标签加入dns-prefetch属性
如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同一域名的tcp连接数量是有限制的(chrome为6个)超过规定数量的tcp连接,则必须要等到之前的请求收到响应后才能继续发送,而http2则可以在一个tcp连接中并发多个请求没有限制,在一些网络较差的环境开启http2性能提升尤为明显
这里极力推荐在支持https协议的服务器中使用http2协议,可以通过web服务器Nginx配置,或是直接让服务器支持http2
nginx开启http2非常简单,在nginx.conf中只需在原来监听的端口后面加上http2就可以了,前提是你的nginx版本不能低于1.95,并且已经开启了https
详解HTTP:入口
vue 的组件化深受大家喜爱,到底组件拆到什么程度算是合理,还要因项目大小而异,小型项目可以简单几个组件搞定,甚至不用 vuex,axios 等等,如果规模较大就要细分组件,越细越好,包括布局的封装,按钮,表单,提示框,轮播等,推荐看下 Element 组件库的代码,没时间写这么详细可以直接用 Element 库,分几点进行优化
组件有明确含义,只处理类似的业务。复用性越高越好,配置性越强越好。
自己封装组件还是遵循配置 props 细化的规则。
组件分类,我习惯性的按照三类划分,page、page-item 和 layout,page 是路由控制的部分,page-item 属于 page 里各个布局块如 banner、side 等等,layout 里放置多个页面至少出现两次的组件,如 icon, scrollTop 等
const Foo =()=>import('./Foo.vue')
const Aoo =()=>import(/* webpackChunkName: "ab90fb63-90a8" */'./Aoo.vue')
constrouter =newVueRouter({
routes: [{
path:'/foo',
component: Foo
},{
path:'/aoo',
component: Aoo
}]
})
webpackChunkName
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。
// template
<test v-if="showTest"></test>
// script
components: {
test: () => import('./test') // 将组件异步引入,告诉webpack,将该部分代码分割打包
},
methods:{
clickTest () {
this.showTest = !this.showTest
}
}
到滚动到可视区域后再去加载, vue-lazyload
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助babel-plugin-component,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例:
安装 babel-plugin-component:
npm install babel-plugin-component -D
将 .babelrc 修改为:
{
"presets": [["es2015", {
"modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
// 使用
import {
Dialog, Loading } from 'element-ui'
Vue.use(Dialog)
看一下Echarts吧,这个对于数据分析也常用:
首先安装babel-plugin-equire : npm i babel-plugin-equire -D
然后,在.babelrc文件中添加该插件
{
"plugins": [
// other plugins
...
"equire"
]
}
创建一个js
// echarts.js
// eslint-disable-next-line
const echarts = equire([
'tooltip',
'candlestick',
'bar',
'line',
'axisPointer',
'legend',
'grid'
])
export default echarts
// 业务组件,引入echarts
import echarts from '@/assets/lib/echarts'
// 使用与以前一样
按需加载echarts
解决vue-cli首屏加载慢的问题
1.只用icon图标库替代图片;
2.使用矢量图svg;
3.webp格式图片;
服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
更好的 SEO: 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
(2)服务端渲染的缺点:
更多的开发条件限制: 例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。
使用webpack-bundle-analyzer 分析打包后的生成的文件结构进行优化
安装完
在package.json配置 “analyz”: “NODE_ENV=production npm_config_report=true npm run build” ,然后运行 npm run analyz 即可,通过分析来具体优化具体内容
productionSourceMap: false,
安装 compression-webpack-plugin
npm install compression-webpack-plugin --save-dev,
这里有个坑,就是如果你的vue版本为2.5.2 及以下 webpack版本为3.6.0及以下时,建议安装compression-webpack-plugin的版本为1.0.0-beta.1 而不是2.0.0,否则可能打包时会报
ValidationError: Compression Plugin Invalid Options
options should NOT have additional properties
将vue项目中的 config/index.js中productionGzip: false改为 productionGzip: true;
productionGzip: true,
productionGzipExtensions: ['js', 'css'],
最后服务端也要开启Gzip,具体服务器具体操作,可以看一下:关于Gzip压缩
CommonsChunkPlugin:
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: '[name].js'
}),
查看dist目录下,新增了一个vendor.js的文件
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3, // 最小混合文件数
}),
extract-text-webpack-plugin:
样式文件分开打包
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].css'),
allChunks: false, // 不打包到一起
}),
MiniCssExtractPlugin:样式压缩抽离
UglifyJsPlugin:删除多余注释等优化代码结构
thread-loader:多线程优化代码打包速度
image-webpack-loader:图片压缩
减少查找模块时间
config.resolve.alias
.set('@', path.resolve(__dirname, './src'))
.set('@views', path.resolve(__dirname, './src/views'))
.set('@components', path.resolve(__dirname, './src/components'))
.set('@assets', path.resolve(__dirname, './src/assets'))
.set('@utils', path.resolve(__dirname, './src/utils'))