Skip to content

Commit 32f1098

Browse files
authored
fix: Optimize the revalidation logic for same key requests. (#4138)
Optimize the revalidation logic for same key requests.
1 parent aeb9363 commit 32f1098

File tree

3 files changed

+191
-9
lines changed

3 files changed

+191
-9
lines changed

e2e/site/app/perf/page.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client'
2+
import { useState } from 'react'
3+
import useSWR from 'swr'
4+
5+
const elementCount = 10_000
6+
const useData = () => {
7+
return useSWR('1', async (url: string) => {
8+
return 1
9+
})
10+
}
11+
12+
const HookUser = () => {
13+
const { data } = useData()
14+
return <div>{data}</div>
15+
}
16+
/**
17+
* This renders 10,000 divs and is used to compare against the render performance
18+
* when using swr.
19+
*/
20+
const CheapComponent = () => {
21+
const cheapComponents = Array.from({ length: elementCount }, (_, i) => (
22+
<div key={i}>{i}</div>
23+
))
24+
return (
25+
<div>
26+
<h2>Cheap Component</h2>
27+
{cheapComponents}
28+
</div>
29+
)
30+
}
31+
32+
/**
33+
* This renders 10,000 divs, each of which uses the same swr hook.
34+
*/
35+
const ExpensiveComponent = () => {
36+
const hookComponents = Array.from({ length: elementCount }, (_, i) => (
37+
<HookUser key={i} />
38+
))
39+
return (
40+
<div>
41+
<h2>Expensive Component</h2>
42+
{hookComponents}
43+
</div>
44+
)
45+
}
46+
47+
export default function PerformancePage() {
48+
const [renderExpensive, setRenderExpensive] = useState(false)
49+
return (
50+
<div>
51+
<h1>Performance Page</h1>
52+
<label>
53+
<input
54+
type="checkbox"
55+
checked={renderExpensive}
56+
onChange={e => setRenderExpensive(e.target.checked)}
57+
/>
58+
Render with swr
59+
</label>
60+
{!renderExpensive ? <CheapComponent /> : <ExpensiveComponent />}
61+
</div>
62+
)
63+
}

e2e/test/perf.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test.describe('performance', () => {
4+
test('should render expensive component within 1 second after checkbox click', async ({
5+
page
6+
}) => {
7+
// Navigate to the perf page
8+
await page.goto('./perf', { waitUntil: 'load' })
9+
10+
// Inject performance measurement into the page
11+
await page.evaluate(() => {
12+
const checkboxInput = document.querySelector(
13+
'input[type="checkbox"]'
14+
) as HTMLInputElement
15+
let expensiveComponentContainer: HTMLElement | null = null
16+
const targetChildCount = 10_000
17+
let startTime = 0
18+
19+
// Track when React starts and completes rendering
20+
const observer = new MutationObserver(mutations => {
21+
for (const mutation of mutations) {
22+
const addedNodes = Array.from(mutation.addedNodes)
23+
for (const node of addedNodes) {
24+
if (node instanceof HTMLElement) {
25+
const h2 = node.querySelector?.('h2')
26+
if (h2?.textContent === 'Expensive Component') {
27+
expensiveComponentContainer = node
28+
console.log('Found Expensive Component container')
29+
}
30+
}
31+
}
32+
33+
if (expensiveComponentContainer) {
34+
const renderedComponents =
35+
expensiveComponentContainer.querySelectorAll('div > div').length
36+
37+
if (renderedComponents % 1000 === 0 && renderedComponents > 0) {
38+
console.log(`Rendered ${renderedComponents} components...`)
39+
}
40+
41+
if (renderedComponents >= targetChildCount) {
42+
console.log(`All ${renderedComponents} components rendered!`)
43+
44+
// Use requestAnimationFrame to ensure the browser has painted
45+
requestAnimationFrame(() => {
46+
requestAnimationFrame(() => {
47+
// Double RAF to ensure we're after the paint
48+
window.performance.mark('expensive-component-painted')
49+
const paintTime = performance.now() - startTime
50+
console.log(
51+
`Total time including paint: ${paintTime.toFixed(2)}ms`
52+
)
53+
})
54+
})
55+
56+
observer.disconnect()
57+
}
58+
}
59+
}
60+
})
61+
62+
observer.observe(document.body, { childList: true, subtree: true })
63+
64+
// Capture more precise timing
65+
checkboxInput.addEventListener(
66+
'click',
67+
() => {
68+
startTime = performance.now()
69+
window.performance.mark('state-change-start')
70+
console.log('Checkbox clicked, state change started')
71+
},
72+
{ once: true }
73+
)
74+
})
75+
76+
// Find and click the checkbox
77+
const checkbox = page.locator('input[type="checkbox"]')
78+
await checkbox.click()
79+
80+
// Wait for the expensive component to be fully rendered
81+
const expensiveComponentHeading = page.locator(
82+
'h2:has-text("Expensive Component")'
83+
)
84+
await expect(expensiveComponentHeading).toBeVisible({ timeout: 60_000 })
85+
86+
// Wait for all components and paint to complete
87+
await page.waitForFunction(
88+
() => {
89+
return (
90+
window.performance.getEntriesByName('expensive-component-painted')
91+
.length > 0
92+
)
93+
},
94+
{ timeout: 5000 }
95+
)
96+
97+
// Get the render time from state change to paint
98+
const renderTime = await page.evaluate(() => {
99+
const startMark =
100+
window.performance.getEntriesByName('state-change-start')[0]
101+
const paintMark = window.performance.getEntriesByName(
102+
'expensive-component-painted'
103+
)[0]
104+
105+
if (!startMark || !paintMark) {
106+
throw new Error('Performance marks not found')
107+
}
108+
109+
return paintMark.startTime - startMark.startTime
110+
})
111+
112+
// Assert that the rendering took less than 1 second (1000ms)
113+
expect(renderTime).toBeLessThan(1000)
114+
})
115+
})

src/index/use-swr.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,8 @@ export const useSWRHandler = <Data = any, Error = any>(
278278

279279
const returnedData = keepPreviousData
280280
? isUndefined(cachedData)
281-
// checking undefined to avoid null being fallback as well
282-
? isUndefined(laggyDataRef.current)
281+
? // checking undefined to avoid null being fallback as well
282+
isUndefined(laggyDataRef.current)
283283
? data
284284
: laggyDataRef.current
285285
: cachedData
@@ -639,13 +639,17 @@ export const useSWRHandler = <Data = any, Error = any>(
639639

640640
// Trigger a revalidation
641641
if (shouldDoInitialRevalidation) {
642-
if (isUndefined(data) || IS_SERVER) {
643-
// Revalidate immediately.
644-
softRevalidate()
645-
} else {
646-
// Delay the revalidate if we have data to return so we won't block
647-
// rendering.
648-
rAF(softRevalidate)
642+
// Performance optimization: if a request is already in progress for this key,
643+
// skip the revalidation to avoid redundant work
644+
if (!FETCH[key]) {
645+
if (isUndefined(data) || IS_SERVER) {
646+
// Revalidate immediately.
647+
softRevalidate()
648+
} else {
649+
// Delay the revalidate if we have data to return so we won't block
650+
// rendering.
651+
rAF(softRevalidate)
652+
}
649653
}
650654
}
651655

0 commit comments

Comments
 (0)