大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 Optimizing A Vue App。
本期共享的是 ------ 在构建 Web App 时优先考虑性能可以改善 UX(用户体验),有助于它们被更多用户使用。本文将引导我们深度学习若干前端优化技巧,让 Vue App 尽量保持高效。
SPA(单页应用程序)在处理实时动态数据时,可以提供丰富的交互式 UX。但 SPA 也可能很笨重臃肿且性能尴尬。我们会介绍若干前端优化技巧,保持 Vue App 相对精简,并且按需交付我们需要的 JS。
粉丝请注意:我们假设您对 Vue 及其组合式 API 有基本认知,但无论您选择哪个框架,都能收获某些有用的东东。
框架的选择
我们选择的 JS 框架是 Vue,部分原因是因为它是我最得心应手的框架。此前,与 React 相比,Vue 的整体包体积更小。虽然但是,自从最近的 React 更新以来,平衡似乎已经向 React 倾斜。这并非兹事体大,因为我们会在本文中了解如何按需导入内容。这两个框架都有优秀的文档和庞大的开发者生态系统,这是另一个考虑因素。
作为演示各种优化的示例,本人构建了一个简单的 Vue App,它从 API 请求数据,并使用 D3.js 渲染若干图表。
我们使用一种人气爆棚的构建工具 Vite 来打包 App,但我们在此介绍的所有优化都适用于大家选择的任何打包程序。
诉诸构建工具树摇优化、压缩和简化
按需发送代码、且开箱即用,是一种优秀实践。Vite 构建时会删除未用的 JS 代码(tree shaking,树摇优化)。Vite 还会压缩打包结果,且可以配置为使用 Gzip/Brotli 压缩输出。
除了压缩之外,我们可以发现未使用作用域提升时,打包体积比优化和压缩的版本高出约 5
倍。因此,无论您使用哪个打包器,平心而论,您可能希望确保它尽量执行优化。
即使我们总体上交付的包较小,浏览器仍然需要时间来解析和编译 JS,这可能会导致 UX 变慢。让我们看看还能做些什么来减少浏览器的工作量。
Vue 组合式 API
Vue 3 引入了组合式 API,这是一组用于创作组件的新型 API,作为选项式 API 的竞品方案。通过专用组合式 API,我们可以按需导入 Vue 函数,而不是整个包。它还使我们能够使用组合式函数编写更多可复用代码。使用组合式 API 编写的代码更适合压缩,且整个 App 更容易 tree-shaking
。
粉丝请注意:如果您使用的是旧版的 Vue,您仍然可以使用组合式 API:它已向后移植到 Vue 2.7,且有一个适用于旧版的官方插件。
依赖导入
一个关键目标是减少客户端下载的初始 JS 包的大小。D3 可用于数据可视化,这是一个大型库,范围广泛。虽然但是,D3 库中有的模块我们根本不需要。如果我们检查整个 D3 包,我们可以发现我们的 App 使用了不到一半的可用模块,甚至可能没有使用这些模块中的所有功能。
保持包体积尽量小的最简单方法之一就是按需导入模块。
以 D3 的 selectAll
函数为例。我们可以从 d3-selection
模块导入我们需要的函数,而不是使用默认导入:
js
// 优化前:
import * as d3 from 'd3'
// 优化后:
import { selectAll } from 'd3-selection'
诉诸动态导入分割代码
动态导入允许我们将模块精准导入到代码中需要的位置,而不是在文件顶部静态导入模块。
js
// 优化前:
import { Auth } from '@aws-amplify/auth'
const user = Auth.currentAuthenticatedUser()
// 优化后:
import('@aws-amplify/auth').then(({ Auth }) => {
const user = Auth.currentAuthenticatedUser()
})
这意味着,该模块会被分割成一个单独的 JS 包或"组块",当且仅当需要时,浏览器才会下载该模块。此外,浏览器可以缓存这些依赖,这些依赖的更改频率可能低于 App 其余部分的代码。
使用 Vue Router 路由懒加载
我们的 App 使用 Vue Router 导航。与动态导入类似,我们可以懒加载路由组件,因此当且仅当用户导航到该路由时,才会导入它们及其相关依赖。
在 index/router.js
文件中:
js
// 优化前:
import Home from "../routes/Home.vue";
import About = "../routes/About.vue";
// 优化后:
const Home = () => import("../routes/Home.vue");
const About = () => import("../routes/About.vue");
const routes = [
{
name: "home",
path: "/",
component: Home,
},
{
name: "about",
path: "/about",
component: About,
},
];
当且仅当用户单击"About"链接、并导航到该路由时,才会加载"About"路由的代码。
异步组件
除了懒加载每个路由之外,我们还可以使用 Vue 的 defineAsyncComponent
方法懒加载单个组件。
js
const KPIComponent = defineAsyncComponent(() => import('../components/KPI.vue'))
这意味着,KPIComponent
的代码会动态导入。我们还可以提供若干在加载或错误状态时显示的组件,如果我们正在加载一个特别大的文件,这特别有用。
拆分 API 请求
我们的 App 主要涉及数据可视化,并且严重依赖于从服务器请求海量数据。其中某些请求可能慢如龟速,因为服务器必须对数据执行大量计算。在最初的原型中,我们对每个路由的 REST API 发出一个请求。不幸的是,这导致用户必须等待很久,有时高达 10
秒,在 App 成功接收数据、并开始渲染可视化之前,用户只能看到加载组件。
我们决定将 API 拆分为多个端点,并对每个小部件发出请求。虽然这可能会增加整体响应时间,但这意味着,App 可以更快使用,因为用户在等待其他页面时,会看到页面的部分内容已渲染。此外,可能发生的任何错误都会被局部化,而页面的其余部分仍然可用。
条件加载组件
现在我们可以将其与异步组件结合起来,这样当且仅当收到服务器的成功响应时,才会加载组件。这里我们正在请求数据,然后在 fetch
函数成功返回时导入组件:
此模式可以扩展到 App 中在用户交互时渲染组件的任意位置。举个栗子,当且仅当用户单击"地图"选项卡时,我们才加载地图组件及其依赖。这称为交互导入。
CSS
如果运行示例代码,我们会看到单击"位置"导航链接会加载地图组件。除了动态导入 JS 模块之外,在组件的 <style>
块中导入依赖也会延迟加载 CSS:
html
// MapView.vue
<style>
@import '../../node_modules/leaflet/dist/leaflet.css';
.map-wrapper {
aspect-ratio: 16 / 9;
}
</style>
中止 API 请求
在一个有大量 API 请求的页面上,如果用户在所有请求完成之前就跑路了,那会怎样?我们可能不希望这些请求继续在后台运行,降低 UX。
我们可以使用 AbortController
接口,它使我们能够按需中止 API 请求。
在 setup
函数中,我们创建一个 controller
,并将其 signal
传递到 fetch
请求参数中:
js
setup(props) {
const controller = new AbortController();
const loadComponent = () => {
return fetch(url, { signal: controller.signal })
.then((response) => response.json())
.then((response) => (data.value = response))
.then(importFunction)
.catch((e) => console.error(e))
.finally(() => (loading.value = false));
};
}
然后,我们使用 Vue 的 onBeforeUnmount
函数在组件卸载前中止请求:
js
onBeforeUnmount(() => controller.abort())
如果我们在请求完成之前运行项目并导航到另一个页面,我们会看到控制台中打印的错误,表明请求已中止。
完结撒花
在构建 Web App 时优先考虑性能可以改善 UX,并有助于它们被更多用户使用。SPA 可以很好地工作,但它们也可能成为性能瓶颈。因此,让我们尝试将它们构建得更好。
本期话题是 ------ 你使用过哪些 Vue 优化的终极小技巧?
欢迎在本文下方群聊自由言论,文明共享。谢谢大家的点赞,掰掰~
《前端 9 点半》每日更新,坚持阅读,自律打卡,每天一次,进步一点。