译者 | 刘涛
审校 | 重楼
我们都曾遭遇过这样的烦恼:漫长的加载界面之后,却只等来了毫无反应的网页。随处可见的加载图标不停旋转,但一切似乎都停滞不前。让我为你描绘一个更生动的画面:
这种情况通常发生的原因是,网站试图在你一落地页面就预取所有必要的数据。可能是正在处理某个API请求,或者多个API在顺序预取数据,导致页面加载延迟。
结果如何?用户体验简直糟糕到极点。你可能会想:"这么大的公司怎么会不注重用户体验?真是令人失望。"因此,用户往往会选择离开网站,这不仅影响了网站的关键指标,还可能导致其收入受到损失。
但是,如果我们能提前预取这些重量级页面的数据,让用户一进入页面就能立即与之交互,会怎样呢?
这就是预取(Prefetching)概念的由来,也正是我们在这篇博文中将深入探讨的内容。
目录
- 预取技术:解决方案
- 预取技术如何提升用户体验
- 问题剖析
- 解决方案一:在父组件中预取数据
- 解决方案二:页面加载时预取数据
- React中如何实现预取
- 过度预取也可能导致性能下降
- 总结
预取技术:解决方案
针对上述问题,我们的目标是在页面加载至网站之前,就预取该页面所需的数据,如此一来,用户在页面加载时便无需再次预取数据。这种技术被称作预取。从技术层面来讲,其定义如下:
预取是一种提前预取所需数据的方法,使主要组件无需等待数据即可加载,从而提升用户体验。
这可以改善用户体验,增强客户对你网站的信任。
预取是一种简洁而优雅的解决方案,相较于标准流程,它更加以用户为中心。要实施预取,我们需要了解用户在网站上的行为。例如,最常访问的页面,或哪些组件在小交互(如悬停)时预取数据。
完成对这些场景的分析后,就可以对其合理应用预取技术。然而,作为开发人员,我们应该谨慎使用这个概念。过度预取也可能降低网站速度,因为你试图为未来场景预取大量数据,这可能会阻塞主页面的数据预取。
预取技术如何提升用户体验
让我们来看几个预取技术有益的场景:
- 为登陆页面上最常访问的链接提前加载数据/页面。例如,假设你有一个"联系我们"链接,此链接为用户最常查看的,且加载时包含大量数据。与其在"联系我们"页面加载时才预取数据,不如在主页就开始预取,这样用户就无需在"联系我们"页面上等待。
- 预取表格数据,用于后续页面。
- 在父组件中预取数据,并将其加载到子组件中。
- 预取数据在弹出窗口中显示。
这些都是在应用中实现预取的方法,它们有助于提升用户体验。
在本文中,我们将讨论最后一个场景:"预取数据在弹出窗口中显示"。这是一个预取技术能够带来的明显好处,为用户提供更流畅体验的典型示例。
问题剖析
让我为你详细阐述这个问题。请想象以下场景:
- 你有一个用于展示特定信息的组件。
- 这个组件内部有一个元素,当鼠标悬停在其上时会显示一个弹出窗口或工具提示。
- 这个弹出窗口在加载时需要预取数据。
现在,设想用户将鼠标悬停在该元素上,随后等待数据被预取并显示在弹出窗口中。在这段等待的时间里,用户会看到一个骨架加载器(Skeleton Loader)。
这个场景大致如下(此为动图,需要下载anigif.ocx控件观看):
每当用户将鼠标悬停在图片上时,他们必须等待很长时间,这真的很令人沮丧:(此为动图,需要下载anigif.ocx控件观看)
要解决此问题,有两种解决方案可供你参考,以帮助你着手并根据自身需求优化解决方案。
解决方案一:在父组件中预取数据
这个解决方案允许你在弹出窗口出现之前便已预取数据,而非在组件加载时预取。
当鼠标悬停在某个元素(如图片)上时,弹出窗口会显现。我们能够在这个元素的父组件上实现鼠标进入时预取数据。鉴于此,在实际需要悬停显示的组件(如图片)之前,我们就已经准备好弹出窗口所需的数据,并将这些数据传递给弹出窗口组件。
这种解决方案并不能完全消除加载状态,不过它能够显著降低用户看到加载状态的概率。(此为动图,需要下载anigif.ocx控件观看)
解决方案二:页面加载时预取数据
这个解决方案受到了类似x.com(可能是指Facebook的前身或类似的大型网站)的数据加载策略的启发,即在弹出窗口组件中,它们在主页面加载时部分预取数据,并在组件挂载时预取剩余的数据。(此为动图,需要下载anigif.ocx控件观看)
正如你从上面的动态图中所看到的,用户的个人资料详细信息在弹出窗口中查看。仔细观察,你会发现与关注者相关的详细信息是稍后预取的。
当你需要在弹出窗口中显示大量数据,但一次性预取所有数据可能对弹出窗口挂载或主页面加载造成较大负担时,采取这种技术就显得非常高效。
一个更好的解决方案是,在主页面上部分加载所需的数据,并在组件挂载时加载剩余的数据。
在这个例子中,我们在鼠标进入图片的父元素时预取了弹出窗口的数据。现在想象一下,一旦弹出窗口的数据加载完成,你还需要预取额外的详细信息。因此,基于上述x.com的方法,我们可以在弹出窗口加载时预取额外的数据。这样做的结果是:
在这里,我们采取以下步骤:
当鼠标进入图片的父组件时,我们预取渲染弹出窗口所必需的主要数据。
这给我们足够的时间来预取主要数据。
在弹出窗口加载时,我们预取另一组数据,即相册数量。当用户阅读姓名和邮箱等信息时,下一组数据已经准备就绪,随时可以展示。
通过这种方式,我们可以做一些小而巧妙的优化,最大限度地减少用户盯着屏幕上的加载动画发呆的时间。
React中如何实现预取
在本部分中,我们将简要介绍如何实现上述预取示例应用程序。
项目设置
要开始创建支持预取功能的应用,请按照以下步骤操作:
你可以使用 Vite.js(这是我使用的工具)或 Create React App 来创建你的应用程序。请在终端中使用以下命令:
yarn create vite prefetch-example --template react-ts
当你用VS Code打开prefetch-example文件夹后,应该会看到如下的文件夹结构。
现在让我们深入了解一下我们将为这个应用构建的组件。
现在,让我们深入了解为这个应用程序所构建的组件。
组件
在此示例中,我们将使用3个组件:
- PopoverExample
- UserProfile
- UserProfileWithFetching
PopoverExample 组件
让我们从第一个组件开始,即PopoverExample。这个组件在界面上展示了一个图像头像(avatar),并在其右侧显示了一些文本。它的布局应该类似于这样:(此为动图,需要下载anigif.ocx控件观看)
该组件的目的是作为一个示例,模拟现实生活中的场景。在这个组件中,当用户将鼠标悬停在图片上时,会加载一个弹出窗口组件。
以下是该组件的代码:
import { useState } from "react";
import { useFloating, useHover, useInteractions } from "@floating-ui/react";
import ContentLoader from "react-content-loader";
import UserProfile from "./UserProfile";
import UserProfileWithFetching from "./UserProfileWithFetching";export const MyLoader = () => (
<ContentLoader
speed={2}
width={340}
height={84}
viewBox="0 0 340 84"
backgroundColor="#d1d1d1"
foregroundColor="#fafafa"
>
<rect x="0" y="0" rx="3" ry="3" width="67" height="11" />
<rect x="76" y="0" rx="3" ry="3" width="140" height="11" />
<rect x="127" y="48" rx="3" ry="3" width="53" height="11" />
<rect x="187" y="48" rx="3" ry="3" width="72" height="11" />
<rect x="18" y="48" rx="3" ry="3" width="100" height="11" />
<rect x="0" y="71" rx="3" ry="3" width="37" height="11" />
<rect x="18" y="23" rx="3" ry="3" width="140" height="11" />
<rect x="166" y="23" rx="3" ry="3" width="173" height="11" />
</ContentLoader>
);
export default function PopoverExample() {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState({});
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: "top",
});
const hover = useHover(context);
const { getReferenceProps, getFloatingProps } = useInteractions([hover]);
const handleMouseEnter = () => {
if (Object.keys(data).length === 0) {
setIsLoading(true);
fetch("https://jsonplaceholder.typicode.com/users/1")
.then((resp) => resp.json())
.then((data) => {
setData(data);
setIsLoading(false);
});
}
};
return (
<div
id="hover-example"
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
textAlign: "left",
}}
onMouseEnter={handleMouseEnter}
>
<span
style={{
padding: "1rem",
}}
>
<img
ref={refs.setReference}
{...getReferenceProps()}
style={{
borderRadius: "50%",
}}
src="https://cdn.jsdelivr.net/gh/alohe/avatars/png/vibrent_5.png"
/>
</span>
<p>
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy text ever
since the 1500s, when an unknown printer took a galley of type and
scrambled it to make a type specimen book. It has survived not only five
centuries, but also the leap into electronic typesetting, remaining
essentially unchanged. It was popularised in the 1960s with the release
of Letraset sheets containing Lorem Ipsum passages, and more recently
with desktop publishing software like Aldus PageMaker including versions
of Lorem Ipsum.
</p>
{isOpen && (
<div
className="floating"
ref={refs.setFloating}
style={{
...floatingStyles,
backgroundColor: "white",
color: "black",
padding: "1rem",
fontSize: "1rem",
}}
{...getFloatingProps()}
>
{isLoading ? (
<MyLoader />
) : (
<UserProfile hasAdditionalDetails {...data} />
)}
{/* <UserProfileWithFetching /> */}
</div>
)}
</div>
);
}
程序运行的过程,让我逐步解释:
- 我们有一个名为“hover-example”的父文件div,其中包含一张图像和一些文本。
- 接下来,我们有条件地渲染了一个具有“floating”类名的 div文件。这便是实际的弹出组件,当你将鼠标悬停在图像上时,它就会开启。我们使用了floating-ui库及其基本的悬停示例来实现弹出窗口的悬停效果。
- 在弹出窗口中,我们有条件地加载UserProfile组件和骨架加载器。当我们正在预取用户资料的数据时,这个骨架加载器便会显现。稍后我会更为详尽地阐释这一点。
- 在 MyLoader 组件中,我们采用了react-content-loader库。该库还设有一个网站,能够帮助你创建加载器。
UserProfile 组件
既然我们已经定义了 Popover 示例,那么此刻便是深入探究 UserProfile 组件细节的时候了。
这个组件出现在Popover组件内部。其目的是加载从JSON占位符API预取的name、email、phone和website详细信息。
为了演示预取示例,我们必须确保UserProfile组件仅作为展示组件存在;也就是说,它内部不包含任何明确的预取逻辑。
关于这个组件的关键点是,数据的预取发生在父组件PopoverExample中。在这个组件里,当鼠标进入该组件(即触发mouseenter事件)时,我们开始预取数据。这是我们之前讨论过的解决方案#1。
这为用户在将鼠标悬停在图像上之前提供了充足的时间来预取数据。以下是相关代码:
import { useEffect, useState } from "react";
import { MyLoader } from "./PopoverExample";
export default function UserProfileWithFetching() {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<Record<string, string>>({});
useEffect(() => {
setIsLoading(true);
fetch("https://jsonplaceholder.typicode.com/users/1")
.then((resp) => resp.json())
.then((data) => {
setData(data);
setIsLoading(false);
});
}, []);
if (isLoading) return <MyLoader />;
return (
<div id="user-profile">
<div id="user-name">name: {data.name}</div>
<div id="user-email">email: {data.email}</div>
<div id="user-phone">phone: {data.phone}</div>
<div id="user-website">website: {data.website}</div>
</div>
);
}
此应用程序的完整代码可在此处找到。
过度预取也可能导致性能下降
一点建议:预取过多并不是一个好主意,原因如下:
- 可能会降低应用速度。
- 如果预取策略不当,会损害用户体验。
预取需要基于用户行为预测:只有当你能通过数据分析预测用户的下一步操作时,预取才是有意义的。比如,如果你能通过用户的历史访问记录预测他们经常访问的页面,那么在这些页面上进行预取就是一个好主意。
因此,请记住要始终策略性地应用预取技术。
总结
在这篇文章中,你了解到实现预取可以显著提升你的Web应用程序的速度和响应性,从而提高用户满意度。
为了进一步阅读,请参考以下文章:
原文标题:How to Boost Web Performance with Prefetching – Improve User Experience by Reducing Load Time,作者:Keyur Paralkar