cache
cho phép bạn lưu trữ kết quả của một lần tìm nạp dữ liệu hoặc tính toán.
const cachedFn = cache(fn);
Tham khảo
cache(fn)
Gọi cache
bên ngoài bất kỳ component nào để tạo ra một phiên bản của hàm có bộ nhớ đệm.
import {cache} from 'react';
import calculateMetrics from 'lib/metrics';
const getMetrics = cache(calculateMetrics);
function Chart({data}) {
const report = getMetrics(data);
// ...
}
Khi getMetrics
được gọi lần đầu tiên với data
, getMetrics
sẽ gọi calculateMetrics(data)
và lưu trữ kết quả vào bộ nhớ đệm. Nếu getMetrics
được gọi lại với cùng một data
, nó sẽ trả về kết quả đã lưu trong bộ nhớ đệm thay vì gọi lại calculateMetrics(data)
.
Tham số
fn
: Hàm bạn muốn lưu kết quả vào bộ nhớ đệm.fn
có thể nhận bất kỳ đối số nào và trả về bất kỳ giá trị nào.
Trả về
cache
trả về một phiên bản đã được lưu trong bộ nhớ đệm của fn
với cùng một kiểu chữ ký. Nó không gọi fn
trong quá trình này.
Khi gọi cachedFn
với các đối số đã cho, nó sẽ kiểm tra trước xem có kết quả đã lưu trong bộ nhớ đệm hay không. Nếu có kết quả đã lưu trong bộ nhớ đệm, nó sẽ trả về kết quả đó. Nếu không, nó sẽ gọi fn
với các đối số, lưu trữ kết quả vào bộ nhớ đệm và trả về kết quả. fn
chỉ được gọi khi có một cache miss.
Lưu ý
- React sẽ làm mất hiệu lực bộ nhớ đệm cho tất cả các hàm đã được ghi nhớ cho mỗi yêu cầu máy chủ.
- Mỗi lần gọi
cache
sẽ tạo ra một hàm mới. Điều này có nghĩa là việc gọicache
với cùng một hàm nhiều lần sẽ trả về các hàm đã được ghi nhớ khác nhau, không dùng chung cùng một bộ nhớ đệm. cachedFn
cũng sẽ lưu trữ các lỗi. Nếufn
đưa ra một lỗi cho các đối số nhất định, nó sẽ được lưu vào bộ nhớ đệm và cùng một lỗi sẽ được đưa ra lại khicachedFn
được gọi với các đối số tương tự.cache
chỉ được sử dụng trong Server Components.
Cách sử dụng
Lưu vào bộ nhớ đệm một phép tính tốn kém
Sử dụng cache
để bỏ qua công việc trùng lặp.
import {cache} from 'react';
import calculateUserMetrics from 'lib/user';
const getUserMetrics = cache(calculateUserMetrics);
function Profile({user}) {
const metrics = getUserMetrics(user);
// ...
}
function TeamReport({users}) {
for (let user in users) {
const metrics = getUserMetrics(user);
// ...
}
// ...
}
Nếu cùng một đối tượng user
được hiển thị trong cả Profile
và TeamReport
, hai component có thể chia sẻ công việc và chỉ gọi calculateUserMetrics
một lần cho user
đó.
Giả sử Profile
được hiển thị trước. Nó sẽ gọi getUserMetrics
, và kiểm tra xem có kết quả đã lưu trong bộ nhớ đệm hay không. Vì đây là lần đầu tiên getUserMetrics
được gọi với user
đó, sẽ có một cache miss. getUserMetrics
sau đó sẽ gọi calculateUserMetrics
với user
đó và ghi kết quả vào bộ nhớ đệm.
Khi TeamReport
hiển thị danh sách users
của nó và đạt đến cùng một đối tượng user
, nó sẽ gọi getUserMetrics
và đọc kết quả từ bộ nhớ đệm.
Chia sẻ ảnh chụp nhanh dữ liệu
Để chia sẻ ảnh chụp nhanh dữ liệu giữa các component, hãy gọi cache
với một hàm tìm nạp dữ liệu như fetch
. Khi nhiều component thực hiện cùng một lần tìm nạp dữ liệu, chỉ một yêu cầu được thực hiện và dữ liệu trả về được lưu vào bộ nhớ đệm và chia sẻ giữa các component. Tất cả các component tham chiếu đến cùng một ảnh chụp nhanh dữ liệu trên toàn bộ quá trình hiển thị máy chủ.
import {cache} from 'react';
import {fetchTemperature} from './api.js';
const getTemperature = cache(async (city) => {
return await fetchTemperature(city);
});
async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}
async function MinimalWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}
Nếu AnimatedWeatherCard
và MinimalWeatherCard
cả hai đều hiển thị cho cùng một city, chúng sẽ nhận được cùng một ảnh chụp nhanh dữ liệu từ hàm đã ghi nhớ.
Nếu AnimatedWeatherCard
và MinimalWeatherCard
cung cấp các đối số city khác nhau cho getTemperature
, thì fetchTemperature
sẽ được gọi hai lần và mỗi vị trí gọi sẽ nhận được dữ liệu khác nhau.
city đóng vai trò là một khóa bộ nhớ đệm.
Tải trước dữ liệu
Bằng cách lưu vào bộ nhớ đệm một lần tìm nạp dữ liệu chạy dài, bạn có thể bắt đầu công việc không đồng bộ trước khi hiển thị component.
const getUser = cache(async (id) => {
return await db.user.query(id);
});
async function Profile({id}) {
const user = await getUser(id);
return (
<section>
<img src={user.profilePic} />
<h2>{user.name}</h2>
</section>
);
}
function Page({id}) {
// ✅ Tốt: bắt đầu tìm nạp dữ liệu người dùng
getUser(id);
// ... một số công việc tính toán
return (
<>
<Profile id={id} />
</>
);
}
Khi hiển thị Page
, component gọi getUser
nhưng lưu ý rằng nó không sử dụng dữ liệu trả về. Lời gọi getUser
sớm này bắt đầu truy vấn cơ sở dữ liệu không đồng bộ xảy ra trong khi Page
đang thực hiện các công việc tính toán khác và hiển thị các component con.
Khi hiển thị Profile
, chúng ta gọi lại getUser
. Nếu lời gọi getUser
ban đầu đã trả về và lưu dữ liệu người dùng vào bộ nhớ đệm, khi Profile
yêu cầu và chờ đợi dữ liệu này, nó có thể chỉ cần đọc từ bộ nhớ đệm mà không yêu cầu một lệnh gọi thủ tục từ xa khác. Nếu yêu cầu dữ liệu ban đầu chưa hoàn thành, việc tải trước dữ liệu theo mẫu này sẽ giảm độ trễ trong việc tìm nạp dữ liệu.
Tìm hiểu sâu
Khi đánh giá một hàm không đồng bộ, bạn sẽ nhận được một Promise cho công việc đó. Promise giữ trạng thái của công việc đó (pending, fulfilled, failed) và kết quả cuối cùng đã được giải quyết của nó.
Trong ví dụ này, hàm không đồng bộ fetchData
trả về một promise đang chờ fetch
.
async function fetchData() {
return await fetch(`https://...`);
}
const getData = cache(fetchData);
async function MyComponent() {
getData();
// ... một số công việc tính toán
await getData();
// ...
}
Khi gọi getData
lần đầu tiên, promise được trả về từ fetchData
được lưu vào bộ nhớ đệm. Các lần tra cứu tiếp theo sau đó sẽ trả về cùng một promise.
Lưu ý rằng lời gọi getData
đầu tiên không có await
trong khi lần thứ hai thì có. await
là một toán tử JavaScript sẽ chờ và trả về kết quả đã được giải quyết của promise. Lời gọi getData
đầu tiên chỉ đơn giản là bắt đầu fetch
để lưu promise vào bộ nhớ đệm cho getData
thứ hai tra cứu.
Nếu đến lần gọi thứ hai promise vẫn đang ở trạng thái pending, thì await
sẽ tạm dừng để chờ kết quả. Tối ưu hóa là trong khi chúng ta chờ fetch
, React có thể tiếp tục với công việc tính toán, do đó giảm thời gian chờ cho lần gọi thứ hai.
Nếu promise đã được giải quyết, cho dù là một lỗi hay kết quả fulfilled, await
sẽ trả về giá trị đó ngay lập tức. Trong cả hai kết quả, đều có một lợi ích về hiệu suất.
Tìm hiểu sâu
Tất cả các API được đề cập đều cung cấp khả năng ghi nhớ, nhưng sự khác biệt là những gì chúng dự định ghi nhớ, ai có thể truy cập bộ nhớ đệm và khi nào bộ nhớ đệm của chúng bị vô hiệu.
useMemo
Nói chung, bạn nên sử dụng useMemo
để lưu vào bộ nhớ đệm một phép tính tốn kém trong một Client Component trên các lần hiển thị. Ví dụ: để ghi nhớ một phép biến đổi dữ liệu trong một component.
'use client';
function WeatherReport({record}) {
const avgTemp = useMemo(() => calculateAvg(record), record);
// ...
}
function App() {
const record = getRecord();
return (
<>
<WeatherReport record={record} />
<WeatherReport record={record} />
</>
);
}
Trong ví dụ này, App
hiển thị hai WeatherReport
với cùng một bản ghi. Mặc dù cả hai component đều thực hiện cùng một công việc, nhưng chúng không thể chia sẻ công việc. Bộ nhớ đệm của useMemo
chỉ cục bộ cho component.
Tuy nhiên, useMemo
đảm bảo rằng nếu App
hiển thị lại và đối tượng record
không thay đổi, mỗi phiên bản component sẽ bỏ qua công việc và sử dụng giá trị đã ghi nhớ của avgTemp
. useMemo
sẽ chỉ lưu vào bộ nhớ đệm phép tính cuối cùng của avgTemp
với các dependency đã cho.
cache
Nói chung, bạn nên sử dụng cache
trong Server Components để ghi nhớ công việc có thể được chia sẻ giữa các component.
const cachedFetchReport = cache(fetchReport);
function WeatherReport({city}) {
const report = cachedFetchReport(city);
// ...
}
function App() {
const city = "Los Angeles";
return (
<>
<WeatherReport city={city} />
<WeatherReport city={city} />
</>
);
}
Viết lại ví dụ trước để sử dụng cache
, trong trường hợp này, phiên bản thứ hai của WeatherReport
sẽ có thể bỏ qua công việc trùng lặp và đọc từ cùng một bộ nhớ đệm như WeatherReport
đầu tiên. Một điểm khác biệt nữa so với ví dụ trước là cache
cũng được khuyến nghị cho ghi nhớ các lần tìm nạp dữ liệu, không giống như useMemo
chỉ nên được sử dụng cho các phép tính.
Tại thời điểm này, cache
chỉ nên được sử dụng trong Server Components và bộ nhớ đệm sẽ bị vô hiệu trên các yêu cầu máy chủ.
memo
Bạn nên sử dụng memo
để ngăn một component hiển thị lại nếu các prop của nó không thay đổi.
'use client';
function WeatherReport({record}) {
const avgTemp = calculateAvg(record);
// ...
}
const MemoWeatherReport = memo(WeatherReport);
function App() {
const record = getRecord();
return (
<>
<MemoWeatherReport record={record} />
<MemoWeatherReport record={record} />
</>
);
}
Trong ví dụ này, cả hai component MemoWeatherReport
sẽ gọi calculateAvg
khi được hiển thị lần đầu tiên. Tuy nhiên, nếu App
hiển thị lại, mà không có thay đổi nào đối với record
, không có prop nào thay đổi và MemoWeatherReport
sẽ không hiển thị lại.
So với useMemo
, memo
ghi nhớ quá trình hiển thị component dựa trên các prop so với các phép tính cụ thể. Tương tự như useMemo
, component đã ghi nhớ chỉ lưu vào bộ nhớ đệm lần hiển thị cuối cùng với các giá trị prop cuối cùng. Khi các prop thay đổi, bộ nhớ đệm sẽ bị vô hiệu và component hiển thị lại.
Khắc phục sự cố
Hàm đã ghi nhớ của tôi vẫn chạy mặc dù tôi đã gọi nó với cùng các đối số
Xem các cạm bẫy đã đề cập trước đó
- Gọi các hàm đã ghi nhớ khác nhau sẽ đọc từ các bộ nhớ đệm khác nhau.
- Gọi một hàm đã ghi nhớ bên ngoài một component sẽ không sử dụng bộ nhớ đệm.
Nếu không có điều nào ở trên áp dụng, thì có thể có vấn đề với cách React kiểm tra xem một cái gì đó có tồn tại trong bộ nhớ đệm hay không.
Nếu các đối số của bạn không phải là kiểu nguyên thủy (ví dụ: đối tượng, hàm, mảng), hãy đảm bảo bạn đang truyền cùng một tham chiếu đối tượng.
Khi gọi một hàm đã ghi nhớ, React sẽ tra cứu các đối số đầu vào để xem kết quả đã được lưu vào bộ nhớ đệm hay chưa. React sẽ sử dụng so sánh nông của các đối số để xác định xem có cache hit hay không.
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// 🚩 Sai: props là một đối tượng thay đổi mỗi lần hiển thị.
const length = calculateNorm(props);
// ...
}
function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}
Trong trường hợp này, hai MapMarker
trông như thể chúng đang thực hiện cùng một công việc và gọi calculateNorm
với cùng một giá trị của {x: 10, y: 10, z:10}
. Mặc dù các đối tượng chứa cùng các giá trị, nhưng chúng không phải là cùng một tham chiếu đối tượng vì mỗi component tạo đối tượng props
riêng của nó.
React sẽ gọi Object.is
trên đầu vào để xác minh xem có cache hit hay không.
import {cache} from 'react';
const calculateNorm = cache((x, y, z) => {
// ...
});
function MapMarker(props) {
// ✅ Tốt: Truyền các kiểu nguyên thủy cho hàm đã ghi nhớ
const length = calculateNorm(props.x, props.y, props.z);
// ...
}
function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}
Một cách để giải quyết vấn đề này có thể là truyền các chiều của vector cho calculateNorm
. Điều này hoạt động vì bản thân các chiều là các kiểu nguyên thủy.
Một giải pháp khác có thể là truyền chính đối tượng vector làm một prop cho component. Chúng ta sẽ cần truyền cùng một đối tượng cho cả hai phiên bản component.
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// ✅ Tốt: Truyền cùng một đối tượng `vector`
const length = calculateNorm(props.vector);
// ...
}
function App() {
const vector = [10, 10, 10];
return (
<>
<MapMarker vector={vector} />
<MapMarker vector={vector} />
</>
);
}