Vue优化预渲染,骨架屏,Nuxt.js服务端渲染

本文最后更新于: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
//在Object上定义反应属性。
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) { //把watcher放到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;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify(); //通知watcher去更新,执行watcher的update
}
});
}

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冻结数据。

1
Object.freeze([{value: 1},{value: 2}])

在数据劫持时,属性不会被配置,不会从新定义

1
2
3
4
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

深冻结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 深冻结函数.
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的语法,动态加载上去。

1
2
3
4
5
6
 const router = new VueRouter({
router: [
{path: '/footer',component: () => import(/* webpackChunkName: "footer" */ '../views/Footer.vue'),},
{path: '/about',component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),},
]
})

动态导入组件,如果我们有一个组件特别复杂,希望用户点了这个按钮,它才弹出来,这时候就可以使用异步组件。
它返回的是一个Promise,它会等待这个组件渲染完成再去显示。

1
2
3
4
5
6
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:在导入解析期间将与之匹配的正则表达式。匹配的任何模块都不会捆绑在一起。

1
2
3
4
5
6
//官方文档中的例子
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加载性能优化 🌓

  1. 第三方模块按需引入,如element-ui。也可以使用babel-plugin-component按需加载组件。

    babel-plugin-component用法和npm官方文档

  2. 图片懒加载,滚动到可视区域动态加载。比如像vue-lazyload;

  3. 滚动渲染可视区域,数据较大时只渲染可视区域,如果一次性渲染太多的节点,可能会挂掉或者卡顿。

    具体用法看再谈前端虚拟列表的实现
    也有现成的插件可以使用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文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

粘贴下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

放上我们的骨架屏

1
2
3
4
5
6
7
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服务端渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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打包优化

  1. 使用cdn方式加载第三方模块,设置externals.

例如,从 CDN 引入 jQuery,而不是把它打包:
index.html

1
2
3
4
5
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
}
  1. 多线程打包happypack
    具体查看这篇文章使用 happypack 提升 Webpack 项目构建速度

  2. splitChunks抽离公共文件

  3. sourceMap的配置

    webpack性能优化具体看webpack各种优化
    webpack-bundle-analyzer 分析打包插件

5. 服务端缓存,客户端缓存

##6. 服务端gzip压缩
可以减小文件体积,传输速度更快。gzip是节省带宽和加快站点速度的有效方法。
具体查看「简明性能优化」双端开启Gzip指南

结束语 🏐

人悄悄,帘外月胧明。「小重山·昨夜寒蛩不住鸣」——岳飞


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!