得益于 Vue 的 响应式系统 和 虚拟 DOM 系统 ,Vue 在渲染组件的过程当中能自动追踪数据的依赖,并精确知晓数据更新的时候哪一个组件须要从新渲染,渲染以后也会通过虚拟 DOM diff 以后才会真正更新到 DOM 上,Vue 应用的开发者通常不须要作额外的优化工做。css
但在实践中仍然有可能遇到性能问题,下面会介绍一些定位分析 Vue 应用性能问题的方式及一些优化的建议。html
总体内容由三部分组成:vue
Vue 应用的性能问题能够分为两个部分,第一部分是运行时性能问题,第二部分是加载性能问题。webpack
和其余 web 应用同样,定位 Vue 应用性能问题最好的工具是 Chrome Devtool,经过 Performance 工具能够用来录制一段时间的 CPU 占用、内存占用、FPS 等运行时性能问题,经过 Network 工具能够用来分析加载性能问题。git
例如,经过 Performance 工具的 Bottom Up 标签咱们能够看出一段时间内耗时最多的操做,这对于优化 CPU 占用和 FPS 太低很是有用,能够看出最为耗时的操做发生在哪里,能够知道具体函数的执行时间,定位到瓶颈以后,咱们就能够作一些针对性的优化。github
更多 Chrome Devtool 使用方式请参考使用 Chrome Devtool 定位性能问题 的指南web
运行时性能主要关注 Vue 应用初始化以后对 CPU、内存、本地存储等资源的占用,以及对用户交互的及时响应。下面是一些有用的优化手段:vuex
开发环境下,Vue 会提供不少警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句没有用,反而会增长应用的体积。有些警告检查还有一些小的运行时开销。chrome
当使用 webpack 或 Browserify 相似的构建工具时,Vue 源码会根据 process.env.NODE_ENV 决定是否启用生产环境模式,默认状况为开发环境模式。在 webpack 与 Browserify 中都有方法来覆盖此变量,以启用 Vue 的生产环境模式,同时在构建过程当中警告语句也会被压缩工具去除。vue-cli
详细的作法请参阅 生产环境部署
当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。一般状况下这个过程已经足够快了,但对性能敏感的应用仍是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,因此构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
详细的作法请参阅 预编译模板
当使用单文件组件时,组件内的 CSS 会以 <style>
标签的方式经过 JavaScript 动态注入。这有一些小小的运行时开销,将全部组件的 CSS 提取到同一个文件能够避免这个问题,也会让 CSS 更好地进行压缩和缓存。
查阅这个构建工具各自的文档来了解更多:
vue-cli
的 webpack 模板已经预先配置好)Object.freeze()
提高性能Object.freeze()
能够冻结一个对象,冻结以后不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data
选项,Vue 将遍历此对象全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setter,这些 getter/setter 对用户来讲是不可见的,可是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
但 Vue 在遇到像 Object.freeze()
这样被设置为不可配置以后的对象属性时,不会为对象加上 setter getter 等数据劫持的方法。参考 Vue 源码
Vue observer 源码
在基于 Vue 的一个 big table benchmark 里,能够看到在渲染一个一个 1000 x 10 的表格的时候,开启Object.freeze()
先后从新渲染的对比。
big table benchmark
开启优化以前
开启优化以后
在这个例子里,使用了 Object.freeze()
比不使用快了 4 倍
Object.freeze()
的性能会更好不使用Object.freeze()
的CPU开销
使用 Object.freeze()
的CPU开销
对比能够看出,使用了 Object.freeze()
以后,减小了 observer 的开销。
Object.freeze()
应用场景因为 Object.freeze()
会把对象冻结,因此比较适合展现类的场景,若是你的数据属性须要改变,能够从新替换成一个新的 Object.freeze()
的对象。
不少时候,咱们会发现接口返回的信息是以下的深层嵌套的树形结构:
{
"id": "123",
"author": {
"id": "1",
"name": "Paul"
},
"title": "My awesome blog post",
"comments": [
{
"id": "324",
"commenter": {
"id": "2",
"name": "Nicole"
}
}
]
}
复制代码
复制代码
假如直接把这样的结构存储在 store 中,若是想修改某个 commenter 的信息,咱们须要一层层去遍历找到这个用户的信息,同时有可能这个用户的信息出现了屡次,还须要把其余地方的用户信息也进行修改,每次遍历的过程会带来额外的性能开销。
假设咱们把用户信息在 store 内统一存放成 users[id]
这样的结构,修改和读取用户信息的成本就变得很是低。
你能够手动去把接口里的信息经过相似数据的表同样像这样存起来,也能够借助一些工具,这里就须要提到一个概念叫作 JSON数据规范化(normalize)
, Normalizr 是一个开源的工具,能够将上面的深层嵌套的 JSON 对象经过定义好的 schema 转变成使用 id 做为字典的实体表示的对象。
举个例子,针对上面的 JSON 数据,咱们定义 users
comments
articles
三种 schema:
import {normalize, schema} from 'normalizr';
// 定义 users schema
const user = new schema.Entity('users');
// 定义 comments schema
const comment = new schema.Entity('comments', {
commenter: user,
});
// 定义 articles schema
const article = new schema.Entity('articles', {
author: user,
comments: [comment],
});
const normalizedData = normalize(originalData, article);
复制代码
复制代码
normalize 以后就能够获得下面的数据,咱们能够按照这种形式存放在 store 中,以后想修改和读取某个 id 的用户信息就变得很是高效了,时间复杂度下降到了 O(1)。
{
result: "123",
entities: {
"articles": {
"123": {
id: "123",
author: "1",
title: "My awesome blog post",
comments: [ "324" ]
}
},
"users": {
"1": { "id": "1", "name": "Paul" },
"2": { "id": "2", "name": "Nicole" }
},
"comments": {
"324": { id: "324", "commenter": "2" }
}
}
}
复制代码
复制代码
须要了解更多请参考 normalizr 的文档 github.com/paularmstro…
当你有让 Vue App 离线可用,或者有接口出错时候进行灾备的需求的时候,你可能会选择把 Store 数据进行持久化,这个时候须要注意如下几个方面:
Vue 社区中比较流行的 vuex-persistedstate,利用了 store 的 subscribe 机制,来订阅 Store 数据的 mutation,若是发生了变化,就会写入 storage 中,默认用的是 localstorage 做为持久化存储。
也就是说默认状况下每次 commit 都会向 localstorage 写入数据,localstorage 写入是同步的,并且存在不小的性能开销,若是你想打造 60fps 的应用,就必须避免频繁写入持久化数据
下面是开发环境下经过 Performance 工具抓取的一个截图,能够看到出现了一次长达 6s 的卡顿:
6秒钟的卡顿
经过 Bottom-Up 能够看到 setState 占用了 3241.4ms 的 CPU 执行时间,而 setState 正是在向 Storage 写入数据。
vuex-persistedstate setState 源码
咱们应该尽可能减小直接写入 Storage 的频率:
因为持久化缓存的容量有限,好比 localstorage 的缓存在某些浏览器只有 5M,咱们不能无限制的将全部数据都存起来,这样很容易达到容量限制,同时数据过大时,读取和写入操做会增长一些性能开销,同时内存也会上涨。
尤为是将 API 数据进行 normalize 数据扁平化后以后,会将一份数据散落在不一样的实体上,下次请求到新的数据也会散落在其余不一样的实体上,这样会带来持续的存储增加。
所以,当设计了一套持久化的数据缓存策略的时候,同时应该设计旧数据的缓存清除策略,例如请求到新数据的时候将旧的实体逐个进行清除。
若是你的应用存在很是长或者无限滚动的列表,那么采用 窗口化 的技术来优化性能,只须要渲染少部分区域的内容,减小从新渲染组件和建立 dom 节点的时间。
vue-virtual-scroll-list 和 vue-virtual-scroller 都是解决这类问题的开源项目。你也能够参考 Google 工程师的文章Complexities of an Infinite Scroller 来尝试本身实现一个虚拟的滚动列表来优化性能,主要使用到的技术是 DOM 回收、墓碑元素和滚动锚定。
Google 工程师绘制的无限列表设计
上面提到的无限列表的场景,比较适合列表内元素很是类似的状况,不过有时候,你的 Vue 应用的超长列表内的内容每每不尽相同,例如在一个复杂的应用的主界面中,整个主界面由很是多不一样的模块组成,而用户看到的每每只有首屏一两个模块。在初始渲染的时候不可见区域的模块也会执行和渲染,带来一些额外的性能开销。
使用组件懒加载在不可见时只须要渲染一个骨架屏,不须要真正渲染组件
你能够对组件直接进行懒加载,对于不可见区域的组件内容,直接不进行加载和初始化,避免初始化渲染运行时的开销。具体能够参考咱们以前的专栏文章 性能优化之组件懒加载: Vue Lazy Component 介绍,了解如何作到组件粒度的懒加载。
在一个单页应用中,每每只有一个 html 文件,而后根据访问的 url 来匹配对应的路由脚本,动态地渲染页面内容。单页应用比较大的问题是首屏可见时间过长。
单页面应用显示一个页面会发送屡次请求,第一次拿到 html 资源,而后经过请求再去拿数据,再将数据渲染到页面上。并且因为如今微服务架构的存在,还有可能发出屡次数据请求才能将网页渲染出来,每次数据请求都会产生 RTT(往返时延),会致使加载页面的时间拖的很长。
服务端渲染、预渲染和客户端渲染的对比
这种状况下能够采用服务端渲染(SSR)和预渲染(Prerender)来提高加载性能,这两种方案,用户读取到的直接就是网页内容,因为少了节省了不少 RTT(往返时延),同时,还能够对一些资源内联在页面,能够进一步提高加载的性能。
能够参考咱们的专栏文章 优化向:单页应用多路由预渲染指南 了解如何利用预渲染进行优化。
服务端渲染(SSR)能够考虑使用 Nuxt 或者按照 Vue 官方提供的 Vue SSR 指南来一步步搭建。
在上面提到的超长应用内容的场景中,经过组件懒加载方案能够优化初始渲染的运行性能,其实,这对于优化应用的加载性能也颇有帮助。
组件粒度的懒加载结合异步组件和 webpack 代码分片,能够保证按需加载组件,以及组件依赖的资源、接口请求等,比起一般单纯的对图片进行懒加载,更进一步的作到了按需加载资源。
使用组件懒加载以前的请求瀑布图
使用组件懒加载以后的请求瀑布图
使用组件懒加载方案对于超长内容的应用初始化渲染颇有帮助,能够减小大量必要的资源请求,缩短渲染关键路径,具体作法请参考咱们以前的专栏文章 性能优化之组件懒加载: Vue Lazy Component 介绍。
本文总结了 Vue 应用运行时以及加载时的一些性能优化措施,下面作一个回顾和归纳:
Vue 应用运行时性能优化措施
Object.freeze()
提高性能Vue 应用加载性能优化措施
文章总结的这些性能优化手段固然不能覆盖全部的 Vue 应用性能问题,咱们也会不断总结和补充其余问题及优化措施,但愿文章中提到这些实践经验能给你的 Vue 应用性能优化工做带来小小的帮助。