譯者 | 劉濤
審校 | 重樓
我們都曾遭遇過這樣的煩惱:漫長的加載界面之后,卻只等來了毫無反應(yīng)的網(wǎng)頁。隨處可見的加載圖標(biāo)不停旋轉(zhuǎn),但一切似乎都停滯不前。讓我為你描繪一個更生動的畫面:
這種情況通常發(fā)生的原因是,網(wǎng)站試圖在你一落地頁面就預(yù)取所有必要的數(shù)據(jù)??赡苁钦谔幚砟硞€API請求,或者多個API在順序預(yù)取數(shù)據(jù),導(dǎo)致頁面加載延遲。
結(jié)果如何?用戶體驗簡直糟糕到極點。你可能會想:"這么大的公司怎么會不注重用戶體驗?真是令人失望。"因此,用戶往往會選擇離開網(wǎng)站,這不僅影響了網(wǎng)站的關(guān)鍵指標(biāo),還可能導(dǎo)致其收入受到損失。
但是,如果我們能提前預(yù)取這些重量級頁面的數(shù)據(jù),讓用戶一進(jìn)入頁面就能立即與之交互,會怎樣呢?
這就是預(yù)?。?/span>Prefetching)概念的由來,也正是我們在這篇博文中將深入探討的內(nèi)容。
目錄
- 預(yù)取技術(shù):解決方案
- 預(yù)取技術(shù)如何提升用戶體驗
- 問題剖析
- 解決方案一:在父組件中預(yù)取數(shù)據(jù)
- 解決方案二:頁面加載時預(yù)取數(shù)據(jù)
- React中如何實現(xiàn)預(yù)取
- 過度預(yù)取也可能導(dǎo)致性能下降
- 總結(jié)
預(yù)取技術(shù):解決方案
針對上述問題,我們的目標(biāo)是在頁面加載至網(wǎng)站之前,就預(yù)取該頁面所需的數(shù)據(jù),如此一來,用戶在頁面加載時便無需再次預(yù)取數(shù)據(jù)。這種技術(shù)被稱作預(yù)取。從技術(shù)層面來講,其定義如下:
預(yù)取是一種提前預(yù)取所需數(shù)據(jù)的方法,使主要組件無需等待數(shù)據(jù)即可加載,從而提升用戶體驗。
這可以改善用戶體驗,增強客戶對你網(wǎng)站的信任。
預(yù)取是一種簡潔而優(yōu)雅的解決方案,相較于標(biāo)準(zhǔn)流程,它更加以用戶為中心。要實施預(yù)取,我們需要了解用戶在網(wǎng)站上的行為。例如,最常訪問的頁面,或哪些組件在小交互(如懸停)時預(yù)取數(shù)據(jù)。
完成對這些場景的分析后,就可以對其合理應(yīng)用預(yù)取技術(shù)。然而,作為開發(fā)人員,我們應(yīng)該謹(jǐn)慎使用這個概念。過度預(yù)取也可能降低網(wǎng)站速度,因為你試圖為未來場景預(yù)取大量數(shù)據(jù),這可能會阻塞主頁面的數(shù)據(jù)預(yù)取。
預(yù)取技術(shù)如何提升用戶體驗
讓我們來看幾個預(yù)取技術(shù)有益的場景:
- 為登陸頁面上最常訪問的鏈接提前加載數(shù)據(jù)/頁面。例如,假設(shè)你有一個"聯(lián)系我們"鏈接,此鏈接為用戶最常查看的,且加載時包含大量數(shù)據(jù)。與其在"聯(lián)系我們"頁面加載時才預(yù)取數(shù)據(jù),不如在主頁就開始預(yù)取,這樣用戶就無需在"聯(lián)系我們"頁面上等待。
- 預(yù)取表格數(shù)據(jù),用于后續(xù)頁面。
- 在父組件中預(yù)取數(shù)據(jù),并將其加載到子組件中。
- 預(yù)取數(shù)據(jù)在彈出窗口中顯示。
這些都是在應(yīng)用中實現(xiàn)預(yù)取的方法,它們有助于提升用戶體驗。
在本文中,我們將討論最后一個場景:"預(yù)取數(shù)據(jù)在彈出窗口中顯示"。這是一個預(yù)取技術(shù)能夠帶來的明顯好處,為用戶提供更流暢體驗的典型示例。
問題剖析
讓我為你詳細(xì)闡述這個問題。請想象以下場景:
- 你有一個用于展示特定信息的組件。
- 這個組件內(nèi)部有一個元素,當(dāng)鼠標(biāo)懸停在其上時會顯示一個彈出窗口或工具提示。
- 這個彈出窗口在加載時需要預(yù)取數(shù)據(jù)。
現(xiàn)在,設(shè)想用戶將鼠標(biāo)懸停在該元素上,隨后等待數(shù)據(jù)被預(yù)取并顯示在彈出窗口中。在這段等待的時間里,用戶會看到一個骨架加載器(Skeleton Loader)。
這個場景大致如下(此為動圖,需要下載anigif.ocx控件觀看):
每當(dāng)用戶將鼠標(biāo)懸停在圖片上時,他們必須等待很長時間,這真的很令人沮喪:(此為動圖,需要下載anigif.ocx控件觀看)
要解決此問題,有兩種解決方案可供你參考,以幫助你著手并根據(jù)自身需求優(yōu)化解決方案。
解決方案一:在父組件中預(yù)取數(shù)據(jù)
這個解決方案允許你在彈出窗口出現(xiàn)之前便已預(yù)取數(shù)據(jù),而非在組件加載時預(yù)取。
當(dāng)鼠標(biāo)懸停在某個元素(如圖片)上時,彈出窗口會顯現(xiàn)。我們能夠在這個元素的父組件上實現(xiàn)鼠標(biāo)進(jìn)入時預(yù)取數(shù)據(jù)。鑒于此,在實際需要懸停顯示的組件(如圖片)之前,我們就已經(jīng)準(zhǔn)備好彈出窗口所需的數(shù)據(jù),并將這些數(shù)據(jù)傳遞給彈出窗口組件。
這種解決方案并不能完全消除加載狀態(tài),不過它能夠顯著降低用戶看到加載狀態(tài)的概率。(此為動圖,需要下載anigif.ocx控件觀看)
解決方案二:頁面加載時預(yù)取數(shù)據(jù)
這個解決方案受到了類似x.com(可能是指Facebook的前身或類似的大型網(wǎng)站)的數(shù)據(jù)加載策略的啟發(fā),即在彈出窗口組件中,它們在主頁面加載時部分預(yù)取數(shù)據(jù),并在組件掛載時預(yù)取剩余的數(shù)據(jù)。(此為動圖,需要下載anigif.ocx控件觀看)
正如你從上面的動態(tài)圖中所看到的,用戶的個人資料詳細(xì)信息在彈出窗口中查看。仔細(xì)觀察,你會發(fā)現(xiàn)與關(guān)注者相關(guān)的詳細(xì)信息是稍后預(yù)取的。
當(dāng)你需要在彈出窗口中顯示大量數(shù)據(jù),但一次性預(yù)取所有數(shù)據(jù)可能對彈出窗口掛載或主頁面加載造成較大負(fù)擔(dān)時,采取這種技術(shù)就顯得非常高效。
一個更好的解決方案是,在主頁面上部分加載所需的數(shù)據(jù),并在組件掛載時加載剩余的數(shù)據(jù)。
在這個例子中,我們在鼠標(biāo)進(jìn)入圖片的父元素時預(yù)取了彈出窗口的數(shù)據(jù)?,F(xiàn)在想象一下,一旦彈出窗口的數(shù)據(jù)加載完成,你還需要預(yù)取額外的詳細(xì)信息。因此,基于上述x.com的方法,我們可以在彈出窗口加載時預(yù)取額外的數(shù)據(jù)。這樣做的結(jié)果是:
在這里,我們采取以下步驟:
當(dāng)鼠標(biāo)進(jìn)入圖片的父組件時,我們預(yù)取渲染彈出窗口所必需的主要數(shù)據(jù)。
這給我們足夠的時間來預(yù)取主要數(shù)據(jù)。
在彈出窗口加載時,我們預(yù)取另一組數(shù)據(jù),即相冊數(shù)量。當(dāng)用戶閱讀姓名和郵箱等信息時,下一組數(shù)據(jù)已經(jīng)準(zhǔn)備就緒,隨時可以展示。
通過這種方式,我們可以做一些小而巧妙的優(yōu)化,最大限度地減少用戶盯著屏幕上的加載動畫發(fā)呆的時間。
React中如何實現(xiàn)預(yù)取
在本部分中,我們將簡要介紹如何實現(xiàn)上述預(yù)取示例應(yīng)用程序。
項目設(shè)置
要開始創(chuàng)建支持預(yù)取功能的應(yīng)用,請按照以下步驟操作:
你可以使用 Vite.js(這是我使用的工具)或 Create React App 來創(chuàng)建你的應(yīng)用程序。請在終端中使用以下命令:
yarn create vite prefetch-example --template react-ts
當(dāng)你用VS Code打開prefetch-example文件夾后,應(yīng)該會看到如下的文件夾結(jié)構(gòu)。
現(xiàn)在讓我們深入了解一下我們將為這個應(yīng)用構(gòu)建的組件。
現(xiàn)在,讓我們深入了解為這個應(yīng)用程序所構(gòu)建的組件。
組件
在此示例中,我們將使用3個組件:
- PopoverExample
- UserProfile
- UserProfileWithFetching
PopoverExample 組件
讓我們從第一個組件開始,即PopoverExample。這個組件在界面上展示了一個圖像頭像(avatar),并在其右側(cè)顯示了一些文本。它的布局應(yīng)該類似于這樣:(此為動圖,需要下載anigif.ocx控件觀看)
該組件的目的是作為一個示例,模擬現(xiàn)實生活中的場景。在這個組件中,當(dāng)用戶將鼠標(biāo)懸停在圖片上時,會加載一個彈出窗口組件。
以下是該組件的代碼:
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文件。這便是實際的彈出組件,當(dāng)你將鼠標(biāo)懸停在圖像上時,它就會開啟。我們使用了floating-ui庫及其基本的懸停示例來實現(xiàn)彈出窗口的懸停效果。
- 在彈出窗口中,我們有條件地加載UserProfile組件和骨架加載器。當(dāng)我們正在預(yù)取用戶資料的數(shù)據(jù)時,這個骨架加載器便會顯現(xiàn)。稍后我會更為詳盡地闡釋這一點。
- 在 MyLoader 組件中,我們采用了react-content-loader庫。該庫還設(shè)有一個網(wǎng)站,能夠幫助你創(chuàng)建加載器。
UserProfile 組件
既然我們已經(jīng)定義了 Popover 示例,那么此刻便是深入探究 UserProfile 組件細(xì)節(jié)的時候了。
這個組件出現(xiàn)在Popover組件內(nèi)部。其目的是加載從JSON占位符API預(yù)取的name、email、phone和website詳細(xì)信息。
為了演示預(yù)取示例,我們必須確保UserProfile組件僅作為展示組件存在;也就是說,它內(nèi)部不包含任何明確的預(yù)取邏輯。
關(guān)于這個組件的關(guān)鍵點是,數(shù)據(jù)的預(yù)取發(fā)生在父組件PopoverExample中。在這個組件里,當(dāng)鼠標(biāo)進(jìn)入該組件(即觸發(fā)mouseenter事件)時,我們開始預(yù)取數(shù)據(jù)。這是我們之前討論過的解決方案#1。
這為用戶在將鼠標(biāo)懸停在圖像上之前提供了充足的時間來預(yù)取數(shù)據(jù)。以下是相關(guān)代碼:
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>
);
}
此應(yīng)用程序的完整代碼可在此處找到。
過度預(yù)取也可能導(dǎo)致性能下降
一點建議:預(yù)取過多并不是一個好主意,原因如下:
- 可能會降低應(yīng)用速度。
- 如果預(yù)取策略不當(dāng),會損害用戶體驗。
預(yù)取需要基于用戶行為預(yù)測:只有當(dāng)你能通過數(shù)據(jù)分析預(yù)測用戶的下一步操作時,預(yù)取才是有意義的。比如,如果你能通過用戶的歷史訪問記錄預(yù)測他們經(jīng)常訪問的頁面,那么在這些頁面上進(jìn)行預(yù)取就是一個好主意。
因此,請記住要始終策略性地應(yīng)用預(yù)取技術(shù)。
總結(jié)
在這篇文章中,你了解到實現(xiàn)預(yù)取可以顯著提升你的Web應(yīng)用程序的速度和響應(yīng)性,從而提高用戶滿意度。
為了進(jìn)一步閱讀,請參考以下文章:
原文標(biāo)題:How to Boost Web Performance with Prefetching – Improve User Experience by Reducing Load Time,作者:Keyur Paralkar