Vue性能优化:如何实现延迟加载和代码拆分?
移动优先方法已经成为一种标准,但不确定的网络条件导致应用程序快速加载变得越来越困难。在本系列文章中,我将深入探讨我们在Storefront应用程序中所使用的Vue性能优化技术,你们也可以在自己的Vue应用程序中使用它们来实现快速加载。
Webpack捆绑的工作原理
本系列文章中的大多数技巧都与如何使JS包变得更小有关。不过,我们首先需要了解Webpack是如何捆绑文件的。
在捆绑文件时,Webpack会创建一个叫作依赖图的东西。它是一种图,链接所有导入的文件。假设Webpack配置中有一个叫作main.js的文件被指定为入口点,那么它就是依赖图的根。这个文件要导入的每个JS模块都将成为图的叶子,而这些叶子中导入的每个模块都将成为叶子的叶子。
Webpack使用这个依赖图来决定应该在输出包中包含哪些文件。输出包是一个JavaScript文件,包含了依赖图中指定的所有模块。
这个过程就像这样:
在知道了捆绑的工作原理之后,我们就可以得出一个结论,即随着项目的增长,初始JavaScript捆绑包也会随着增大,下载和解析捆绑包所需的时间也会越长,用户等待的时间也会变长,他们离开网站的可能性也就越大。
简单地说,更大的捆绑包=更少的用户,至少在大多数情况下是这样的。
延迟加载
那么,在添加新功能和改进应用程序的同时,我们如何减小捆绑包的大小?答案很简单——延迟加载和代码拆分。
顾名思义,延迟加载就是延迟加载应用程序的部分内容。换句话说——只在真正需要它们时加载它们。代码拆分是指将应用程序拆分成可以延迟加载的块。
在大多数情况下,你不需要在用户访问网站后立即使用JavaScript包中的所有代码。假设应用程序中有三个不同的路由,无论用户最终要访问哪个更难,总是要下载、解析和执行所有这些路由,即使他们只需要其中的一个路由。多么浪费时间和精力!
延迟加载允许我们拆分捆绑包,并只提供必要的部分,这样用户就不会浪费时间下载和解析无用的代码。
要想知道网站实际使用了多少JavaScript代码,我们可以转到devtools -\u0026gt; cmd + shift + p -\u0026gt; type coverage -\u0026gt; 单击“record”,然后应该能够看到实际使用了多少下载的代码。
标记为红色的都是当前路由不需要的东西,可以延迟加载。如果你使用了源映射,可以单击列表中的任意一个文件,看看是哪些部分没有被调用到。可以看到,即使是vuejs.org也还有很大的改进空间。
通过延迟加载适当的组件和库,我们将Storefront的捆绑包大小减少了60%!
接下来,让我们来看看如何在Vue应用程序中使用延迟加载。
动态导入
我们可以使用Webpack动态导入来加载应用程序的某些部分。让我们看看它们的工作原理以及它们与常规导入的区别。
标准的JS模块导入:
// main.jsimport ModuleA from './module_a.js'ModuleA.doStuff()
它将作为main.js的叶子被添加到依赖图中,并被捆绑到捆绑包中。
但是,如果我们仅在某些情况下需要ModuleA呢?将这个模块与初始捆绑包捆绑在一起不是一个好主意,因为可能根本就不需要它。我们需要一种方法来告诉应用程序应该在什么时候下载这段代码。
这个时候可以使用动态导入!来看一下这个例子:
//main.jsconst getModuleA = () =\u0026gt; import('./module_a.js')// invoked as a response to some user interactiongetModuleA() .then({ doStuff } =\u0026gt; doStuff())
我们来看看这里都发生了什么:
我们创建了一个返回import()函数的函数,而不是直接导入module_a.js。现在Webpack会将动态导入模块的内容捆绑到一个单独的文件中,除非调用了这个函数,否则import()也不会被调用,也就不会下载这个文件。在后面的代码中,我们下载了这个可选的代码块,作为对某些用户交互的响应。
通过使用动态导入,我们基本上隔离了将被添加到依赖图中的叶子(在这里是module_a),并在需要时下载它(这意味着我们也切断了在module_a.js中导入的模块)。
让我们看另一个可以更好地说明这种机制的例子。
假设我们有4个文件:main.js、module_a.js、module_b.js和module_c.js。要了解动态导入的原理,我们只需要main和module_a的源代码:
//main.jsimport ModuleB from './mobile_b.js'const getModuleA = () =\u0026gt; import('./module_a.js')getModuleA() .then({ doStuff } =\u0026gt; doStuff())//module_a.jsimport ModuleC from './module_c.js'
通过让module_a成为一个动态导入的模块,可以让module_a及其所有子文件从依赖图中分离。当module_a被动态导入时,其中导入的所有子模块也会被加载。
换句话说,我们为依赖图创建了一个新的入口点。
延迟加载Vue组件
我们已经知道了什么是延迟加载以及为什么需要它,现在是时候看看如何在Vue应用程序中使用它了。
好消息是它非常简单,我们可以延迟加载整个SFC以及它的css和html,语法和之前一样!
const lazyComponent = () =\u0026gt; import('Component.vue')
现在只会在请求组件时才会下载它。以下是调用Vue组件动态加载的最常用方法:
- 调用带有import语句的函数:
const lazyComponent = () =\u0026gt; import('Component.vue')lazyComponent()
- 请求渲染组件:
\u0026lt;template\u0026gt; \u0026lt;div\u0026gt; \u0026lt;lazy-component /\u0026gt; \u0026lt;/div\u0026gt;\u0026lt;/template\u0026gt;\u0026lt;script\u0026gt;const lazyComponent = () =\u0026gt; import('Component.vue')export default { components: { lazyComponent }}\u0026lt;/script\u0026gt;
请注意,只有当请求在模板中渲染组件时,才会调用lazyComponent函数。
例如这段代码:
\u0026lt;lazy-component v-if=\u0026quot;false\u0026quot; /\u0026gt;
就不会动态导入组件,因为它没有被添加到DOM(但一旦值变为true就会导入,这是一种条件延迟加载Vue组件的好方法)。
应用程序增长
vue-router是一个可用于将Web应用程序拆分为单独页面的库。每个页面都变成与某个特定URL路径相关联的路由。
假设我们有一个简单的组合应用程序,具有以下结构:
你可能已经注意到,根据我们访问的路由的不同,可能不需要Home.vue或About.vue,但它们都在相同的app.js捆绑包中,无论用户访问哪个路由,它们都会被下载。这真是浪费下载和解析时间!
只是额外下载一个路由这并不是什么大问题,但想象一下,当这个应用程序越来越大,任何新添加的内容都意味着在首次访问时需要下载更大的捆绑包。
用户有可能在1秒钟之内就会离开我们的网站,所以这是不可接受的!
使用vue-router进行基于路由的代码拆分
为了避免让应用程序变得更糟,我们只需要使用动态导入语法为每个路由创建单独的包。
与Vue中的其他东西一样——它非常简单。我们不需要直接将组件导入到route对象中,只需要传入一个动态导入函数。只有在解析给定的路由时,才会下载路由组件。
所以不要像这样静态导入路径组件:
import RouteComponent form './RouteComponent.vue'const routes = [{ path: /foo', component: RouteComponent }]
我们需要动态导入它,这将创建一个新的捆绑包,并将这个路由作为入口点:
const routes = [ { path: /foo', component: () =\u0026gt; import('./RouteComponent.vue') }]
使用动态导入的捆绑和路由是这个样子的:
Webpack将创建三个包:
app.js——主捆绑包,包含应用程序入口点(main.js)和每个路由所需的库或组件;
home.js——包含主页的捆绑包,当用户输入/路径时才会加载;
about.js——包含关于页面的捆绑包,当用户输入/about路径时才会加载。
这项技术几乎适用于所有应用程序,并且可以提供非常好的结果。
在很多情况下,基于路由的代码拆分将解决所有的性能问题,并且可以在几分钟内应用于几乎任何一个应用程序上!
Vue生态系统中的代码拆分
你可能正在使用Nuxt或vue-cli来创建应用程序。如果是这样,你就应该知道,它们都有一些与代码拆分有关的自定义行为:
- 在vue-cli 3中,默认情况下将预取所有延迟加载的块。
- 在Nuxt中,如果我们使用了Nuxt路由系统,所有页面路由默认都是经过代码拆分的。
现在让我们来看看一些常见的反模式,它会减小基于路由的代码拆分所起到的作用。
第三方捆绑反模式
第三方捆绑通常被用在单独JS文件包含node_modules模块的上下文中。
虽然把所有东西放在一个地方并缓存它们可能很诱人,但这种方法也引入了我们将所有路由捆绑在一起时遇到的问题:
看到了问题吗?即使我们只在一个路由中使用lodash,它也会与所有其他依赖项一起被捆绑在vendor.js中,因此它总是会被加载。
将所有依赖项捆绑在一个文件中看起来很诱人,但这样会导致应用程序加载时间变长。但我们可以做得更好!
让应用程序使用基于路由的代码拆分就足以确保只下载必要的代码,只是这样会导致一些重复代码。
假设Home.vue也需要lodash。
在这种情况下,从/about(About.vue)导航到/(Home.vue)需要下载lodash两次。
不过这仍然比下载大量的冗余代码要好,但既然已经有了同样的依赖项,就应该重用它,不是吗?
这个时候可以使用splitChunksPlugin。
只需在Webpack配置中添加几行代码,就可以将公共依赖项分组到一个单独的包中,并共享它们。
// webpack.config.jsoptimization: { splitChunks: { chunks: 'all' }}
我们通过chunks属性告诉Webpack应该优化哪些代码块。这里设置为all,这意味着它应该优化所有的代码。
Vuex模块的两种类型
在进一步了解如何延迟加载Vuex模块之前,你需要了解有哪些方法可用来注册Vuex模块,以及它们的优缺点。
静态Vuex模块在Store初始化期间声明。以下是显式创建静态模块的示例:
// store.jsimport { userAccountModule } from './modules/userAccount'const store = new Vuex.Store({ modules: { user: userAccountModule }})
上面的代码将创建一个带有静态模块userAccountModule的Vuex Store。静态模块不能取消注册,并且在Store初始化后不能更改它们的结构。
虽然这种限制对于大多数模块来说都不是问题,并且在一个地方声明所有这些限制确实有助于将所有与数据相关的内容放在一个地方,但这种方法也有一些缺点。
假设我们的应用程序中有一个带有专用Vuex模块的Admin Dashboard。
// store.jsimport { userAccountModule } from './modules/userAccount'import { adminModule } from './modules/admin'const store = new Vuex.Store({ modules: { user: userAccountModule, admin: adminModule }})
你可以想象这样的模块可能非常庞大。尽管仪表盘只会被一小部分用户使用,但由于静态Vuex模块的集中注册,它的所有代码都将被包含在主捆绑包中。
这肯定不是我们想要的结果。我们需要一种方法,只为/admin路由加载这个模块。你可能已经猜到静态模块无法满足我们的需求。所有静态模块都需要在创建Vuex Store时注册,所以不能到了后面再进行注册。
这个时候可以使用动态模块!
动态模块可以在创建Vuex Store后进行注册。这个功能意味着我们不需要在应用程序初始化时下载动态模块,并且可以将其捆绑在不同的代码块中,或者在需要时延迟加载。
首先让我们来看一下之前的代码如果使用了动态注册的admin模块将会是什么样子。
// store.jsimport { userAccountModule } from './modules/userAccount'import { adminModule } from './modules/admin'const store = new Vuex.Store({ modules: { user: userAccountModule, }})store.registerModule('admin', adminModule)
我们没有将adminModule对象直接传给Store的modules属性,而是使用registerModule方法在Store创建后注册它。
动态注册不需要在模块内部进行任何更改,因此可以静态或动态注册任意的Vuex模块。
Vuex模块的代码拆分
让我们回到我们的问题。既然我们知道如何动态注册admin模块,当然可以尝试将它的代码放入/admin路由捆绑包中。
让我们先暂停一下,先简要了解一下我们的应用程序。
// router.jsimport VueRouter from 'vue-router'const Home = () =\u0026gt; import('./Home.vue')const Admin = () =\u0026gt; import('./Admin.vue')const routes = [ { path: '/', component: Home }, { path: '/admin', component: Admin }]export const router = new VueRouter({ routes })
在router.js中,我们有两个延迟加载并经过代码拆分的路由。admin Vuex模块仍然在主app.js捆绑包中,因为它是在store.js中静态导入的。
让我们修复这个问题,只将这个模块发送给访问/admin路由的用户,这样其他用户就不会下载冗余代码。
为此,我们将在/admin路由组件中加载admin模块,而不是在store.js中导入和注册它。
// store.jsimport { userAccountModule } from './modules/userAccount'export const store = new Vuex.Store({ modules: { user: userAccountModule, }})// Admin.vueimport adminModule from './admin.js'export default { // other component logic mounted () { this.$store.registerModule('admin', adminModule) }, beforeDestroy () { this.$store.unregisterModule('admin') }}
我们来看看都发生了什么!
我们先是在Admin.vue(/admin route)导入和注册admin Store,等到用户退出管理面板,我们就取消注册该模块,以防止同一模块被多次注册。
现在,因为admin模块是在Admin.vue(而不是store.js)中导入的,所以它将与经过代码拆分的Admnin.vue捆绑在一起!
现在我们知道如何使用动态Vuex模块注册将特定于路由的模块分发到适当的捆绑包中。让我们来看看稍微复杂一些的场景。
延迟加载Vuex模块
假设Home.vue上有客户评价部分,我们希望显示客户对服务的积极评价。因为有很多,所以我们不想在用户进入网站后立即显示它们,而是在用户需要查看时才显示它们。我们可以添加一个“Show Testimonials”按钮,点击这个按钮后将加载并显示客户评价。
为了保存客户评价数据,我们还需要另外一个Vuex模块,我们把它叫作testimonials。这个模块将负责显示之前添加的评价和添加新的评价,但我们不需要了解实现细节。
我们希望只在用户单击了按钮后才下载testimonials模块,因为在这之前不需要它。让我们来看看如何利用动态模块注册和动态导入来实现这个功能。Testimonials.vue是Home.vue中的一个组件。
让我们快速过一下代码。
当用户单击Show Testimonials按钮时,将调用getTestimonials()方法。它负责调用getTestimonialsModule()来获取testimonials.js。在promise完成之后(意味着模块已加载),我们就会动态注册它,并触发负责获取客户评价的动作。
testimonials.js被捆绑到一个单独的文件中,只有在调用getTestimonialsModule方法时才会下载这个文件。
当我们退出管理面板时,只是在beforeDestroy生命周期hook中取消了之前注册的模块,如果再次进入这个路由,就不会重复加载。
参考链接:
https://itnext.io/vue-js-app-performance-optimization-part-1-introduction-to-performance-optimization-and-lazy-29e4ff101019
https://itnext.io/vue-js-app-performance-optimization-part-2-lazy-loading-routes-and-vendor-bundle-anti-pattern-4a62236e09f9
https://itnext.io/vue-js-app-performance-optimization-part-3-lazy-loading-vuex-modules-ed67cf555976
更多内容,请关注前端之巅。
相关文章:

GitHub怎样fork别人代码到自己仓库并进行贡献
在过程中可能遇到这个问题:https://www.cnblogs.com/q1104460935/p/8275833.html 这个博客应该可以解决 比如说现在有一个很牛逼的项目,我们进入项目地址, 想将这个项目复制到自己的github仓库,然后你还想将 仓库中的代码拉取到…

python数据结构与算法:单向循环列表
单向循环列表:python实现,及其对应的 增删查检 操作 ##################### P4.9-P4.12 循环链表 ########################### #coding:utf-8 class Node(object):def __init__(self,elem):self.elem elemself.next None class SinglecycleList(ob…

http权威指南-http连接管理
2019独角兽企业重金招聘Python工程师标准>>> HTTP连接管理 浏览器解析URL流程: 浏览器解析出域名;浏览器查询这个主机名的IP地址;浏览器获得端口号;浏览器发起到主机名IP地址端口的80连接;浏览器向服务器发…

在macos上基于python2.7安装PyQt5
在macos上基于python2.7安装PyQt5 在python3上面安装PyQt5是十分简单的,可是,在python2.7上安装这个东西,着实让人折腾了一把。要总结一下,年纪大了,记性不好。 首先要安装最新版的Qt和python2,命令如下&am…

python数据结构与算法:二分查找
二分查找:python 实现def binary_seaech(alist,item):"""二分查找 递归实现"""n len(alist)if n > 0:mid n // 2if alist[mid] item:return Trueelif item < alist[mid]:return binary_seaech(alist[:mid],item)else:retur…

使用maven镜像
综述 用maven做项目,最郁闷的莫过于某些依赖库下载不了。被墙了,你懂的。使用maven镜像仓库及其重要,特别是国内的镜像,可以有效缓解被墙疼痛。 常用的镜像 国外镜像 ibiblio.org <mirror> <id>ibiblio</id> &…

Jupyter Notebook 快捷键(基本)
Jupyter Notebook 快捷键 Jupyter Notebook 有两种键盘输入模式。编辑模式,允许你往单元中键入代码或文本;这时的单元框线是绿色的。命令模式,键盘输入运行程序命令;这时的单元框线是灰色。 命令模式 (按键 Esc 开启) Enter : …

关于二叉树的几个必须掌握的实现
The best way to end your fear is to face it yourself. 结束恐惧的最佳办法就是自己面对。本分分享了二叉搜索树的几种实现,由简入繁。说句题外话,马上又是金三银四的季节了,无论跳不跳槽,增加自己的知识储备总是没错的。从代码…

python数据结构与算法:队列与双端队列
双端队列: #################队列#################### #coding:utf-8 """ Deque() 创建一个空的双端队列 add_front(item) 从队头加入一个item元素 add_rear(item) 从队尾加入一个item元素 remove_front() 从队头删除一个item元素 remove_rear() 从…

view5.3登录桌面提示当前可用桌面资源不足
问题描述:用户反馈有个桌面经常提示当前可用桌面资源不足,开始的时候反复重启还可以使用,今天发现彻底无法登录了。解决方法:首先登录到view administrator管理平台查看该桌面发现状态是可用,说明桌面正常,…

【HDU】4706 Children's Day(模拟)
http://acm.hdu.edu.cn/showproblem.php?pid4706 该题没有输入,直接输出不同形状大小的N,在输出不同形状N的时候是要用到26个字母,并且是循环输出 #include <iostream>using namespace std;char map[60][60]; char a[] "abcdef…

详解原生AJAX请求demo(兼容IE5/6)
function createXHR(){ // 检测原生XHR对象是否存在if ( window.XMLHpptRequest ){// 如果存在就返回新实例return new XMLHpptRequest();} else { // 如果不存在就检测ActiveX对象// 兼容IE5/6return new ActiveXObject(Microsoft.XMLHttp);} }// 在所有的浏览器中创建XHR对象…

【POJ】3268 Silver Cow Party (将有向图的边反转)
问题链接:http://poj.org/problem?id3268 【问题描述】 One cow from each of N farms (1 ≤ N ≤ 1000) conveniently numbered 1…N is going to attend the big cow party to be held at farm #X (1 ≤ X ≤ N). A total of M (1 ≤ M ≤ 100,000) unidirectio…

项目管理深入理解08--成本管理
成本管理一章非常的重要,尤其是对于程序员来说,这方面非常的薄弱,但这部分知识无论是在项目管理中还是日常生活中都灰常重要,不然很难成为一个财务自由的程序员。此外,由于财务方面知识点比较多,特增加经济…

python数据结构与算法:双向链表
双向链表: ###################### P4.13-P4. 双向链表 ########################### # import singlelinkListclass Node(object):def __init__(self,item):self.elem itemself.next Noneself.prev None# class DoublelinkList(singlelinkList): #继承 class …

如何开发一个区块链应用程序
区块链是一项巧妙的发明,有望使数字世界更加安全和分散。通过允许数字信息的分发而不是复制,区块链技术创建了一种新型互联网。最初是为数字货币比特币而设计的,现在科技界正在寻找该技术的其他潜在用途。在不久的将来,我们将看到…

python数据结构与算法:栈
栈: Stack() 创建一个新的空栈 push(item) 添加一个新的元素item到栈顶 pop() 弹出栈顶元素 peek() 返回栈顶元素 is_empty() 判断栈是否为空 size() 返回栈的元素个数 Stack() 创建一个新的空栈 push(item) 添加一个新的元素item到栈顶 pop() 弹出栈顶元素 peek(…

【PAT (Basic Level) 】1014 福尔摩斯的约会 (20 分)
大侦探福尔摩斯接到一张奇怪的字条: 我们约会吧! 3485djDkxh4hhGE 2984akDfkkkkggEdsb s&hgsfdk d&Hyscvnm大侦探很快就明白了,字条上奇怪的乱码实际上就是约会的时间星期四 14:04,因为前面两字符串中第 1 对相同的…

菜鸟物流云是如何帮助快递合作伙伴解决双11巨大业务负荷的?
物流云双11 双11前,菜鸟物流云共接入12家合作伙伴,全部参加双11大促活动,作为物流云的首次双11,尤其是经过了快递公司的大考经验,事实证明项目是靠谱的。 双11前已经整体上云的快递合作伙伴2家,韵达和天天&…

安装H3C的各种问题
HCL安装完成后,启动HCL失败;提示:“当前系统用户名中包含非ASCII字符”问题?HCL只能安装装在英文路径下,如果用户名为中文或者安装路径有中文目录,就会出现此问题,请确保系统用户名和安装路径中…

前景背景分割——ostu算法的原理及实现 OpenCV (八)
OpenCV 【八】——前景背景分割——ostu算法的原理及实现 实验结果代码实现实现原理参考资料实验结果 代码实现 #include<opencv2/opencv.hpp> #include<iostream> using namespace std; using namespace cv; //计算图像灰度直方图 Mat calcgrayhist(const Mat&am…

【PAT (Basic Level) 】1015 德才论 (25 分)
宋代史学家司马光在《资治通鉴》中有一段著名的“德才论”:“是故才德全尽谓之圣人,才德兼亡谓之愚人,德胜才谓之君子,才胜德谓之小人。凡取人之术,苟不得圣人,君子而与之,与其得小人࿰…

浏览器启动外部软件
常可以看见使用浏览器代码启动本地应用的软件.例如qq、迅雷、等等.那么他们是怎么做到的呢? 它的奥秘:Register protocol 前言我们经常看到 tencent://..thunder://这两种开头的网址,往往觉得很奇怪,很想弄懂其中的原理,是如何实现的&#x…

Luogu P1082 同余方程(NOIP 2012) 题解报告
题目传送门 【题目大意】 求关于x的同余方程 ax≡1(mod b)的最小整数解。 【思路分析】 由同余方程的有关知识可得,ax≡1(mod b)可以化为axby1,此方程有解当且仅当gcd(a,b)1,于是就可以用欧几里得算法求出一组特解x0,y0。 那么x0就…

MATLAB【二】————图像做减法,批量文本处理,子图显示
clear; clc; close all;name_string ["1.5ms\100\" ];length strlength(name_string); [m,n] size(length);%%----------------------------- for num1:mstr name_string(num,1); figure(color, [1, 1, 1], position, [0, 0, 1800,800]); % 为区分边界&a…

与数据有关的问题
◆ 背景说明 在为用户排查问题,解决问题时,有一种情况是不容易引起大家注意的,那就是用户的数据;比如,数据中有某些特殊字符,引起展现不了或展现不正常;现在ÿ…

【PAT (Basic Level) 】1024 科学计数法 (20 分)
科学计数法是科学家用来表示很大或很小的数字的一种方便的方法,其满足正则表达式 [][1-9].[0-9]E[][0-9],即数字的整数部分只有 1 位,小数部分至少有 1 位,该数字及其指数部分的正负号即使对正数也必定明确给出。 现以科学计数法…

jsp 实栗 jsp + jdbc 登录
jsp 实栗 jsp jdbc 实现登录 实现思路 一个表单页,输入用户登录和密码,然后信息提交到jsp页面进行验证,如果可以服务器跳转到登录成功页,失败,跳转到错误页 跳转的时候窗口的URL地址会发生变化 代码如下 编写登录代码…

OpenCV 【十】——Gamma校正 ——图像灰度变化
Gamma校正(C、OpenCV实现) 1.作用: Gamma校正是对输入图像灰度值进行的非线性操作,使输出图像灰度值与输入图像灰度值呈指数关系: 伽玛校正由以下幂律表达式定义: 2.函数原型 void calcHist( const Mat*…

Linux磁盘阵列技术详解(二)--raid 1创建
我在Linux磁盘阵列技术详解(一)里已经详细介绍了几种RAID磁盘阵列方式,原理以及创建raid 0 的详细步骤。那么这篇文档就着重讲解如何创建raid 1的技术:步骤如下:① 分区同样我们还是以一块硬盘的不同分区为例ÿ…