本文最后更新于:a few seconds ago
1. 编码优化🍞
1.data属性
data中的数据都会增加getter和setter,会收集对应的watcher,只有在需要渲染到视图上的才需要放到data,所以没有响应式需求的不要放在data里面。
defineReactive源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| function defineReactive ( obj, key, val, customSetter, shallow ) { var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } var getter = property && property.get; if (!getter && arguments.length === 2) { val = obj[key]; } var setter = property && property.set;
var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); }
|
2.SPA页面采用keep-alive缓存组件
keep-alive会缓存我们的组件,它会把组件缓存到内存中,下一次访问的时候,会从缓存中拿出来。
keep-alive是个函数式组件,里面有个render()方法。他会把默认的插槽拿出来,找到第一个组件(里面只能放一个组件)。
拿到后先去判断是否有include和exclude,这两个是判断缓存哪一些,不缓存哪一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) // 获取第一个组件节点 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode }
// 缓存vnode const { cache, keys } = this const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { //如果有缓存,直接将缓存返回 vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) } else { cache[key] = vnode //缓存下来下次用 keys.push(key) // 超过缓存限制,就从第一个开始删除 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } }
vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } }
|
3.拆分组件
不拆分和拆分有什么区别呢?
vue有个特点,它是按组件刷新的。数据一变,就会刷新当前组件,如果都写到一起了,如果数据一变,整个组件都要刷新。
拆分之后,一个组件的数据变了,可以只更新那个小组件。核心就是:减少不必要的渲染(尽可能细化拆分组件)
还有就是提高复用性,增加代码可维护性。
4.v-if
当前值为false时内部指令不会执行,具有阻断功能。比如说面板,弹框,里面包含很多逻辑,用户不点我们可以使里面先不执行。
5.key保证唯一性
· vue默认采用就地复用原则,可以加key保证唯一性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> <div id="app"> <div id="nav"> <button @click="show =! show">按钮</button> <input type="text" v-if="show" :key="1"> <input type="password" v-else :key="2"> </div> </div> </template> <script> export default { data() { return { show: true, }; }, }; </script>
|
· 如果数据项的顺序被改变,Vue不会移动DOM元素来匹配数据项的顺序。它默认会比对内容,如果内容有变,就会多次去创建DOM,删除DOM。
这样的性能消耗更大,所以我们如果写循环代码,尽量用唯一的key来实现。这里面主要是DOM Diff的策略。
· 应该使用数据的id作为key的属性。
6.Object.freeze
vue会实现数据劫持,给每个数据增加getter和setter,如果希望数据只是用来展示到页面上而已,并不需要改数据视图会刷新。
这样的话,就可以用Object.freeze冻结数据。
| Object.freeze([{value: 1},{value: 2}])
|
在数据劫持时,属性不会被配置,不会从新定义
| const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return }
|
深冻结
| // 深冻结函数. function deepFreeze(obj) { // 取回定义在obj上的属性名 var propNames = Object.getOwnPropertyNames(obj); // 在冻结自身之前冻结属性 propNames.forEach(function(name) { var prop = obj[name]; // 如果prop是个对象,冻结它 if (typeof prop == 'object' && prop !== null) deepFreeze(prop); }); // 冻结自身(no-op if already frozen) return Object.freeze(obj); }
|
7.路由懒加载,异步组件
动态加载组件,依赖webpack-codespliting
功能,不能单独去用,它会拆分这个路由。
比如:当我们这个路由匹配到了,它会调用这个函数将路由动态加载上去。
webpack如果遇到import语法,会单独打包出js文件,加载的时候使用JSONP
的语法,动态加载上去。
| const router = new VueRouter({ router: [ {path: '/footer',component: () => import(/* webpackChunkName: "footer" */ '../views/Footer.vue'),}, {path: '/about',component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),}, ] })
|
动态导入组件,如果我们有一个组件特别复杂,希望用户点了这个按钮,它才弹出来,这时候就可以使用异步组件。
它返回的是一个Promise,它会等待这个组件渲染完成再去显示。
| import Search from "./Search"; export default { components: { Search: () => import("./Search"); } };
|
注意
如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。
但是这里有个坑,不能直接传入一个变量,下面是概要,具体看官方文档
无法使用完全动态的import语句,例如import(foo)。因为foo可能是系统或项目中任何文件的任何路径。这样把全部文件打包了是会报错的。
我们还可以使用webpackInclude和webpackExclude选项,来减少webpack导入的文件数量,他们接受一个正则表达式。
webpackInclude和webpackExclude选项不会干扰前缀。例如:./locale。
webpackInclude
:在导入解析期间将与之匹配的正则表达式。仅将匹配的模块捆绑在一起。
webpackExclude
:在导入解析期间将与之匹配的正则表达式。匹配的任何模块都不会捆绑在一起。
| //官方文档中的例子 import( /* webpackInclude: /\.json$/ */ /* webpackExclude: /\.noimport\.json$/ */ `./locale/${language}` );
|
8.runtime运行时
在开发时尽量采用单文件的方式(.vue),他不需要我们运行时去编译template。
webpack打包时会将模板进行编译(vue-template-compiler)
但是如果使用new Vue({template}),里面的template是在代码运行的时候去编译模板,对性能有损耗。
9.数据持久化问题
可以使用vuex-persist
进行数据持久化,因为我们vue里的数据,一刷新就会丢,所以我们要把数据存到localStorage里。
但是如果频繁的对localStorage进行操作,对性能的损耗也很大,vuex-persist
提供了一个过滤功能来解决这个问题,您可以过滤掉不想引起存储更新的任何改变。
第二个方法是进行节流。
vuex-persist具体用法看其的GitHub文档;
window.localStorage(在PC重新启动后仍然存在,直到您清除浏览器数据为止)
window.sessionStorage(关闭浏览器选项卡时消失)
2. vue加载性能优化 🌓
第三方模块按需引入,如element-ui。也可以使用babel-plugin-component按需加载组件。
babel-plugin-component用法和npm官方文档
图片懒加载,滚动到可视区域动态加载。比如像vue-lazyload;
滚动渲染可视区域,数据较大时只渲染可视区域,如果一次性渲染太多的节点,可能会挂掉或者卡顿。
具体用法看再谈前端虚拟列表的实现
也有现成的插件可以使用vue-scroll。
3. 用户体验👍
1. app-skeleton
配置webpack插件 vue-skeleton-webpack-plugin
单页骨架屏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import Vue from 'vue' // 引入的骨架屏组件 import skeletonHome from './skeleton/skeletonHome.vue' export default new Vue({ components: { skeletonHome, }, template: `<skeletonHome/> ` }); plugins: [ new SkeletonWebpackPlugin({ // 我们编写的插件 webpackConfig: { entry: { app: require('./src/entry-skeleton.js') } } }) ]
|
带路由的骨架屏,编写skeleton.js文件
| import Vue from 'vue'; import Skeleton1 from './Skeleton1'; import Skeleton2 from './Skeleton2';
export default new Vue({ components: { Skeleton1, Skeleton2 }, template: ` <div> <skeleton1 id="skeleton1" style="display:none"/> <skeleton2 id="skeleton2" style="display:none"/> </div> ` });
|
configureWebpack里的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: path.join(__dirname, './src/skeleton.js'), }, }, router: { mode: 'history', routes: [ { path: '/', skeletonId: 'skeleton1' }, { path: '/about', skeletonId: 'skeleton2' }, ] }, minimize: true, quiet: true, })
|
具体用法
先来创建一个单页的骨架屏
1. 先在根目录创建一个vue.config.js
粘贴下面代码
| let SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin'); const path = require('path'); module.exports = { configureWebpack:{ plugins:[ new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: path.resolve('./src/entry-skeleton.js') } } }) ] } }
|
2. 创建./src/entry-skeleton.js
放上我们的骨架屏
| import Vue from 'vue';
export default new Vue({ render() { return <h1>hello Vue</h1>; }, });
|
这样就ok啦
用npm run serve 试试!
参考
为vue项目添加骨架屏
基于 vue-skeleton-webpack-plugin 的骨架屏实战
姜文大佬实现的原理
实现骨架屏插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class MyPlugin { apply(compiler) { compiler.plugin('compilation', (compilation) => { compilation.plugin( 'html-webpack-plugin-before-html-processing', (data) => { data.html = data.html.replace(`<div id="app"></div>`, ` <div id="app"> <div id="home" style="display:none">首页 骨架屏</div> <div id="about" style="display:none">about页面骨架屏</div> </div> <script> if(window.hash == '#/about' || location.pathname=='/about'){ document.getElementById('about').style.display="block" }else{ document.getElementById('home').style.display="block" } </script> `); return data; } ) }); } }
|
2. app-shell
一般配合PWA使用,百度的Lavas。
使用serviceWorker,第一次加载,第二次到本地。
4. SEO优化方案 🏃
1.vue的预渲染插件
npm install prerender-spa-lpugin
缺陷是数据不够动态,可以使用ssr服务端渲染
| const path = require('path') const PrerenderSPAPlugin = require('prerender-spa-plugin')
module.exports = { plugins: [ ... new PrerenderSPAPlugin({ // Required - The path to the webpack-outputted app to prerender. staticDir: path.join(__dirname, 'dist'), // Required - Routes to render. routes: [ '/', '/about', '/some/deep/nested/route' ], }) ] }
|
里面主要用到一个包是puppeteer,他是一个无头浏览器,运行它会打开一个浏览器,但是你看不见它,
它会先将页面放在浏览器上去跑,然后生成节点,渲染成HTML,一般用作e2e,或爬虫。
2. 服务端渲染
概念:放在浏览器进行就是浏览器渲染,放在服务器进行就是服务器渲染。就跟以前的模板渲染一样。
客户端渲染不利于SEO搜索引擎优化
服务端渲染是可以被爬虫抓取到的,客户端异步渲染是很难被爬虫抓取到的
SSR直接将HTML轴向传递给浏览器。大大加快了首屏加载时间。
SSR占用更多的CPU和内存资源
一些常用的浏览器API可能无法正常使用
在vue中只支持beforeCreate和created两个生命周期
3. 什么是nuxt
Nuxt.js是使用Webpack和Node.js进行封装的基于Vue的SSR框架
nuxt特点
优点:
更好的SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。首屏渲染速度快
缺点:
Node.js中渲染完整的应用程序,显然只比提供静态文件的服务器更多占用CPU资源。需要考虑服务器负载,缓存策略
具体查看nuxt.js官方文档
4. webpack打包优化
- 使用cdn方式加载第三方模块,设置externals.
例如,从 CDN 引入 jQuery,而不是把它打包:
index.html
| <script src="https://code.jquery.com/jquery-3.1.0.js" integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk=" crossorigin="anonymous"> </script>
|
webpack.config.js
| module.exports = { //... externals: { jquery: 'jQuery' } }
|
多线程打包happypack
具体查看这篇文章使用 happypack 提升 Webpack 项目构建速度
splitChunks抽离公共文件
sourceMap的配置
webpack性能优化具体看webpack各种优化
webpack-bundle-analyzer 分析打包插件
5. 服务端缓存,客户端缓存
##6. 服务端gzip压缩
可以减小文件体积,传输速度更快。gzip是节省带宽和加快站点速度的有效方法。
具体查看「简明性能优化」双端开启Gzip指南
结束语 🏐
人悄悄,帘外月胧明。「小重山·昨夜寒蛩不住鸣」——岳飞