譯者 | 劉濤
審校 | 重樓
我們都曾遭遇過這樣的煩惱:漫長的加載界面之后,卻只等來了毫無反應的網頁。隨處可見的加載圖標不停旋轉,但一切似乎都停滯不前。讓我為你描繪一個更生動的畫面:
這種情況通常發生的原因是,網站試圖在你一落地頁面就預取所有必要的數據。可能是正在處理某個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