随着网络应用的复杂性和规模不断增加,性能问题也随之而来。开发人员通常通过采用服务器端渲染(SSR)来解决这个问题,以卸载客户端的一些渲染过程。
然而,即使HTML渲染发生在服务器上,网站的性能仍然会受到影响。虽然HTML是以快速和有利于SEO的方式交付的,但水化的过程--使应用程序在客户端互动--可能是昂贵的。反过来,对于具有复杂的、深度嵌套的HTML的应用程序来说,交互时间(TTI)和估计输入延迟(EIL)等指标会急剧下降。
现在,你可以用代码分割等技术来解决这个问题,或者立即加载应用程序的重要部分,同时延迟代码的交付,并将其他组件水化。这可能会提高你的指标,但仍然会在用户从未看到或互动的组件上浪费加载时间。
这就是懒惰的水合作用的地方。让我们看看这是什么,它如何工作,以及如何在Vue 3中实现它。
局部水化与懒惰水化
要了解水合的变体以及它们是如何工作的,你首先需要熟悉部分水合的情况。
顾名思义,在部分水合中,你只对你的应用程序的某些部分进行水合。这在实现所谓的"岛屿架构 "时很有用,在这种情况下,不同的应用程序部分被视为独立的实体。这使得应用程序的每个部分都独立于其他部分,这使得它们可以分别进行水合。
让我们想一想,部分水合和岛屿架构将如何适用于一个网站,如博客。你可以对工具栏和评论区等互动部分进行水化,但让其他部分如内容本身完全静态化。这种方法提高了网站的性能和用户体验,而且没有资源浪费在静态内容上,使互动部分的水化速度更快。
懒惰的水化是建立在部分水化的概念之上,并将它们更进一步。对于任何已经包含SSR、基本水合和async组件的框架来说,这个概念在实现上是相似的。
你不再只能够决定网络应用的哪些部分应该被水化,你还可以决定什么时候应该发生。例如,你可以只在空闲时、在视口中、或在响应其他各种触发器(如Promise resolving或用户互动)时对组件进行水化。
这将资源节约和性能优化提升到另一个层次。你不再需要对用户永远不会看到或与之互动的组件进行水化,这使得TTI几乎是即时的
Vue的懒惰水化
Vue 2有一个伟大的、相当流行的库,名为vue-lazy-hydration。它提供了一个无渲染的LazyHydrate 组件和一堆手动函数包装器,如hydrateWhenVisible ,用于包装你想懒惰水化的组件。它还允许你在不同的条件下进行补水,比如说。
- 当浏览器处于空闲状态时(使用
requestIdleCallback)。 - 当组件在视口内时(使用
IntersectionObserver)。 - 在用户互动时(
click,mouseover, 等)。 - 通过手动触发(
Promise, 布尔开关,等等) never(对于静态的、仅有SSR的组件)。
遗憾的是,在发表文章时,这个以及其他任何著名的懒惰水化库都不支持Vue 3。尽管如此,Vue-lazy-hydration对Vue 3的支持正在开发中,似乎有计划在Nuxt 3出来后发布。
这使得我们要么继续使用Vue 2进行懒惰补水,要么实现我们自己的机制,这就是我们在这篇文章中要做的。
在Vue 3中实现懒惰的水合作用
像Vue这样的UI框架,有内置的SSR和水合支持,实现懒惰水合是相当容易的。
你需要一个包装器或无渲染组件,在服务器上自动渲染你的组件,同时在客户端使用条件渲染来延迟水化,直到满足某些条件。
我决定在react-lazy-hydration的基础上实现Vue 3的懒惰水化。它的代码比vue-lazy-hydration的简单,而且出乎意料地更容易翻译,React Hooks与Vue Composition API转换得很好。
组件声明和道具
我们从一个基本的Vue 3组件开始,包含额外的TypeScript和一个isBrowser 实用函数,用于检查浏览器的globals是否可用。
<script lang="ts">
import { defineComponent, onMounted, PropType, ref, watch } from "vue";
type VoidFunction = () => void;
const isBrowser = () => {
return typeof window === "object";
};
export default defineComponent({
props: {},
setup() {},
});
</script>
<template></template>
我们的lazy hydration包装器将包括与前面提到的库所提供的类似的功能。为此,我们将不得不接受一套相当广泛的配置道具。
// ...
export default defineComponent({
props: {
ssrOnly: Boolean,
whenIdle: Boolean,
whenVisible: [Boolean, Object] as PropType<
boolean | IntersectionObserverInit
>,
didHydrate: Function as PropType<() => void>,
promise: Object as PropType<Promise<any>>,
on: [Array, String] as PropType<
(keyof HTMLElementEventMap)[] | keyof HTMLElementEventMap
>,
},
// ...
});
// ...
有了上述道具,我们将支持仅有SSR的静态组件,以及在浏览器空闲时、组件可见时或给定的Promise 解决后进行水化。
除此之外,on 将支持在用户交互时进行水化,而didHydrate 将允许在组件水化后进行回调。
设置功能
在setup ,我们首先初始化一些必要的值。
// ...
export default defineComponent({
// ...
setup() {
const noOptions =
!props.ssrOnly &&
!props.whenIdle &&
!props.whenVisible &&
!props.on?.length &&
!props.promise;
const wrapper = ref<Element | null>(null);
const hydrated = ref(noOptions || !isBrowser());
const hydrate = () => {
hydrated.value = true;
};
},
});
// ...
我们将使用一个wrapper 模板引用来访问包装元素,以及一个hydrated 引用来保存反应布尔值,该值决定了当前的水合状态。
注意我们是如何初始化hydrated ref的。当没有设置任何选项时,默认情况下组件将立即进行水化。否则,在通过SSR时,水化将在客户端被延迟。
hydrate 只是一个单向的辅助函数,用于设置hydrated 到true 。
补水回调注册
接下来,我们开始创建逻辑,有一个onMounted 回调和一个watch 效果。
// ...
onMounted(() => {
if (wrapper.value && !wrapper.value.hasChildNodes()) {
hydrate();
}
});
watch(
hydrated,
(hydrate) => {
if (hydrate && props.didHydrate) props.didHydrate();
},
{ immediate: true }
);
// ...
在onMounted 回调中,我们检查该元素是否有任何孩子。如果没有,我们可以立即进行水化。
watch 效果处理didHydrate 回调。注意immediate 选项--它对水合不延迟时很重要,在SSR期间和没有提供选项时都是如此。
设置主要的watch 效果
现在,我们进入主要的watch 效果,它将处理所有的选项并适当地设置hydrated ref。
// ...
watch(
[() => props, wrapper, hydrated],
(
[{ on, promise, ssrOnly, whenIdle, whenVisible }, wrapper, hydrated],
_,
onInvalidate
) => {
if (ssrOnly || hydrated) {
return;
}
const cleanupFns: VoidFunction[] = [];
const cleanup = () => {
cleanupFns.forEach((fn) => {
fn();
});
};
if (promise) {
promise.then(hydrate, hydrate);
}
},
{ immediate: true }
);
// ...
该效果将触发道具的变化,以及wrapper 和hydrate refs中的变化。
首先,我们检查该组件是否只在服务器端呈现,或者是否已经被水化。我们这样做是因为,在这两种情况下,都不需要进一步评估效果,所以我们可以从函数中return 。
如果这个过程继续下去,我们将初始化清理函数,以便在效果无效时,处理Promise-based lazy hydration。
基于可见性的水化
接下来,仍然在效果内部,我们处理基于可见性的水合。如果支持IntersectionObserver ,我们初始化它,传递默认或提供的选项。否则,我们立即进行水合。
// ...
if (whenVisible) {
if (wrapper && typeof IntersectionObserver !== "undefined") {
const observerOptions =
typeof whenVisible === "object"
? whenVisible
: {
rootMargin: "250px",
};
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting || entry.intersectionRatio > 0) {
hydrate();
}
});
}, observerOptions);
io.observe(wrapper);
cleanupFns.push(() => {
io.disconnect();
});
} else {
return hydrate();
}
}
// ...
注意清理回调,将IntersectionObserver 实例与wrapper 元素断开连接。
基于浏览器空闲的水化
我们对基于浏览器空闲的水合采用类似的结构,这次是用requestIdleCallback 和cancelIdleCallback 。
if (whenIdle) {
if (typeof window.requestIdleCallback !== "undefined") {
const idleCallbackId = window.requestIdleCallback(hydrate, {
timeout: 500,
});
cleanupFns.push(() => {
window.cancelIdleCallback(idleCallbackId);
});
} else {
const id = setTimeout(hydrate, 2000);
cleanupFns.push(() => {
clearTimeout(id);
});
}
}
requestIdleCallback 的跨浏览器兼容性低于80%,尤其是iOS和macOS上的Safari不支持,所以我们必须用setTimeout 实现回退,延迟水合并将其推送到异步队列中。
如果你使用TypeScript,你应该注意,目前,你不会在默认库中找到requestIdleCallback 。为了正确打字,你需要安装@types/requestidlecallback。
基于用户交互的水化
最后,我们处理基于用户事件的水合。在这里,事情相对简单,因为我们只是循环处理事件并相应地设置事件监听器。
if (on) {
const events = ([] as Array<keyof HTMLElementEventMap>).concat(on);
events.forEach((event) => {
wrapper?.addEventListener(event, hydrate, {
once: true,
passive: true,
});
cleanupFns.push(() => {
wrapper?.removeEventListener(event, hydrate, {});
});
});
}
onInvalidate(cleanup);
之后,记得调用onInvalidate 来注册清理函数,效果就准备好了!
完成模板的制作
为了完成这个组件,从setup 函数中返回模板中需要的引用。
// ...
export default defineComponent({
// ...
setup() {
// ...
return {
wrapper,
hydrated,
};
},
});
// ...
然后,在模板中,渲染包装<div> ,分配参数,并有条件地渲染懒惰水化的组件。
<template>
<div ref="wrapper" :style="{ display: 'contents' }" v-if="hydrated">
<slot></slot>
</div>
<div ref="wrapper" v-else></div>
</template>
使用我们的懒惰水合组件
我们的懒惰水合组件已经准备好了,现在是时候测试它了
构建我们的Vue 3 SSR应用程序的脚手架
首先,你需要建立一个可以使用SSR或静态网站生成器(SSG)的环境。从技术上讲,任何预渲染的HTML和启用了水化功能的Vue 3都可以使用,但你的里程可能会有所不同。
由于Nuxt.js和Gridsome都还没有与Vue 3兼容,你最好的选择是使用Vite-plugin-ssr之类的东西。这样的解决方案将允许你利用Vite提供的伟大的开发经验,同时实现SSR,而不会有太多麻烦。
你可以用下面的命令搭建一个新的vit-plugin-ssr应用程序。
npm init vite-plugin-ssr@latest
然后,用上面的指南或GitHub的Gist来设置懒惰的水化组件。
有了这些,去任何一个可用的页面,在<LazyHydrate> ,然后玩一个交互式的组件!使用不同的选项,看看什么时候组件会出现。
<template>
<h1>Welcome</h1>
This page is:
<ul>
<li>Rendered to HTML.</li>
<li>
Interactive. <LazyHydrate when-visible><Counter /></LazyHydrate>
</li>
</ul>
</template>
<script lang="ts">
import Counter from "./_components/Counter.vue";
import LazyHydrate from "./_components/LazyHydrate.vue";
export default {
components:{
Counter,
LazyHydrate,
}
};
</script>
使用不同的选项,查看组件的互动性,查看它何时被didHydrate 回调水化,以及更多!
将懒惰的水化与异步组件相结合
为了进一步改善你的应用程序的TTI指标和加载时间,你可以将懒惰的水化与异步组件结合起来。这将把你的应用程序分割成更小的块,准备按需加载。有了这个,你的懒惰水化组件将只在水化发生时加载。
import { defineAsyncComponent } from "vue";
import LazyHydrate from "./_components/LazyHydrate.vue";
export default {
components: {
Counter: defineAsyncComponent({
loader: () => import("./_components/Counter.vue"),
}),
LazyHydrate,
},
};
请记住,你必须小心使用这种方法,因为动态获取组件可能会给用户带来明显的延迟。在这种情况下,你必须有选择地推迟哪些组件,并需要实现后备内容,比如在获取和解析代码时的加载器。
然而,即使考虑到所有这些,懒惰的异步组件仍然有很大的潜力,可以大幅提高大型复杂应用程序的性能,特别是那些严重依赖交互式图表或隐藏对话框等元素的应用程序。
底线
所以,你已经拥有了它--在Vue 3中解释和实现的懒惰的水合作用!通过本帖实现的组件,你可以优化你的SSR/SSG应用,提高其性能、响应速度和用户体验。
关于<LazyHydrate> 组件的完整代码,请查看GitHub Gist。请随意使用它进行实验。如果你有任何改进的想法,请在GitHub上告诉我。
请务必关注vue-lazy-hydration上的更新。下一个版本据说会利用新的Vue 3-Node APIs,因此可能会比本帖的简单实现更有性能或提供更多的功能。
The postAchieving lazy hydration in Vue 3 from scratchappeared first onLogRocket Blog.