本文共 7762 字,大约阅读时间需要 25 分钟。
移动优先方法已经成为一种标准,但不确定的网络条件导致应用程序快速加载变得越来越困难。在本系列文章中,我将深入探讨我们在应用程序中所使用的Vue性能优化技术,你们也可以在自己的Vue应用程序中使用它们来实现快速加载。
本系列文章中的大多数技巧都与如何使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应用程序中使用它了。
好消息是它非常简单,我们可以延迟加载整个SFC以及它的css和html,语法和之前一样!
const lazyComponent = () =\u0026gt; import('Component.vue')
现在只会在请求组件时才会下载它。以下是调用Vue组件动态加载的最常用方法:
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中的其他东西一样——它非常简单。我们不需要直接将组件导入到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路径时才会加载。
这项技术几乎适用于所有应用程序,并且可以提供非常好的结果。
在很多情况下,基于路由的代码拆分将解决所有的性能问题,并且可以在几分钟内应用于几乎任何一个应用程序上!
你可能正在使用Nuxt或vue-cli来创建应用程序。如果是这样,你就应该知道,它们都有一些与代码拆分有关的自定义行为:
现在让我们来看看一些常见的反模式,它会减小基于路由的代码拆分所起到的作用。
第三方捆绑通常被用在单独JS文件包含node_modules模块的上下文中。
虽然把所有东西放在一个地方并缓存它们可能很诱人,但这种方法也引入了我们将所有路由捆绑在一起时遇到的问题:
看到了问题吗?即使我们只在一个路由中使用lodash,它也会与所有其他依赖项一起被捆绑在vendor.js中,因此它总是会被加载。
将所有依赖项捆绑在一个文件中看起来很诱人,但这样会导致应用程序加载时间变长。但我们可以做得更好!
让应用程序使用基于路由的代码拆分就足以确保只下载必要的代码,只是这样会导致一些重复代码。
假设Home.vue也需要lodash。
在这种情况下,从/about(About.vue)导航到/(Home.vue)需要下载lodash两次。
不过这仍然比下载大量的冗余代码要好,但既然已经有了同样的依赖项,就应该重用它,不是吗?
这个时候可以使用。
只需在Webpack配置中添加几行代码,就可以将公共依赖项分组到一个单独的包中,并共享它们。
// webpack.config.jsoptimization: { splitChunks: { chunks: 'all' }}
我们通过chunks属性告诉Webpack应该优化哪些代码块。这里设置为all,这意味着它应该优化所有的代码。
在进一步了解如何延迟加载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模块。
让我们回到我们的问题。既然我们知道如何动态注册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模块注册将特定于路由的模块分发到适当的捆绑包中。让我们来看看稍微复杂一些的场景。
假设Home.vue上有客户评价部分,我们希望显示客户对服务的积极评价。因为有很多,所以我们不想在用户进入网站后立即显示它们,而是在用户需要查看时才显示它们。我们可以添加一个“Show Testimonials”按钮,点击这个按钮后将加载并显示客户评价。
为了保存客户评价数据,我们还需要另外一个Vuex模块,我们把它叫作testimonials。这个模块将负责显示之前添加的评价和添加新的评价,但我们不需要了解实现细节。
我们希望只在用户单击了按钮后才下载testimonials模块,因为在这之前不需要它。让我们来看看如何利用动态模块注册和动态导入来实现这个功能。Testimonials.vue是Home.vue中的一个组件。
让我们快速过一下代码。
当用户单击Show Testimonials按钮时,将调用getTestimonials()方法。它负责调用getTestimonialsModule()来获取testimonials.js。在promise完成之后(意味着模块已加载),我们就会动态注册它,并触发负责获取客户评价的动作。
testimonials.js被捆绑到一个单独的文件中,只有在调用getTestimonialsModule方法时才会下载这个文件。
当我们退出管理面板时,只是在beforeDestroy生命周期hook中取消了之前注册的模块,如果再次进入这个路由,就不会重复加载。
参考链接:
更多内容,请关注前端之巅。
转载地址:http://gjttx.baihongyu.com/