-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf: lazy prefetching to avoid negative performance impact for the f…
…irst page (#602)
- Loading branch information
1 parent
01ed9e2
commit bcf880f
Showing
14 changed files
with
409 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 0 additions & 23 deletions
23
packages/platform-web/src/shuvi-app/react/utils/requestIdleCallback.ts
This file was deleted.
Oops, something went wrong.
5 changes: 4 additions & 1 deletion
5
packages/platform-web/src/shuvi-app/react/utils/useIntersection.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
packages/utils/src/__tests__/idleCallback/awaitPageLoadAndIdle.browser.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
|
||
describe('awaitPageLoadAndIdle', () => { | ||
let originalRequestIdleCallback: typeof window.requestIdleCallback; | ||
let originalCancelIdleCallback: typeof window.cancelIdleCallback; | ||
|
||
beforeAll(() => { | ||
// Save the original `requestIdleCallback` and `cancelIdleCallback` for restoration | ||
originalRequestIdleCallback = window.requestIdleCallback; | ||
originalCancelIdleCallback = window.cancelIdleCallback; | ||
|
||
Object.defineProperty(document, 'readyState', { | ||
value: 'complete', | ||
writable: true | ||
}); | ||
}); | ||
|
||
beforeEach(() => { | ||
// Restore mocks after each test | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
afterEach(() => { | ||
// Restore mocks after each test | ||
jest.clearAllMocks(); | ||
window.requestIdleCallback = originalRequestIdleCallback; | ||
window.cancelIdleCallback = originalCancelIdleCallback; | ||
// @ts-expect-error test purpose | ||
document.readyState = 'loading'; // Reset the readyState | ||
}); | ||
|
||
it('should resolve immediately if the page is already loaded and idle time condition is met', async () => { | ||
expect.assertions(2); | ||
|
||
// @ts-expect-error test purpose | ||
document.readyState = 'complete'; | ||
|
||
window.requestIdleCallback = jest.fn(callback => { | ||
const fakeDeadline = { timeRemaining: () => 50 } as IdleDeadline; | ||
return setTimeout(() => callback(fakeDeadline), 10) as unknown as number; | ||
}); | ||
window.cancelIdleCallback = jest.fn(id => { | ||
clearTimeout(id); | ||
}); | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
const { awaitPageLoadAndIdle } = await import('../../idleCallback'); | ||
await expect(awaitPageLoadAndIdle()).resolves.toBeUndefined(); | ||
expect(window.cancelIdleCallback).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
it('should wait until the page load event is triggered', async () => { | ||
expect.assertions(2); | ||
|
||
// @ts-expect-error test purpose | ||
document.readyState = 'loading'; | ||
|
||
window.requestIdleCallback = jest.fn(callback => { | ||
const fakeDeadline = { timeRemaining: () => 50 } as IdleDeadline; | ||
return setTimeout(() => callback(fakeDeadline), 10) as unknown as number; | ||
}); | ||
window.cancelIdleCallback = jest.fn(id => { | ||
clearTimeout(id); | ||
}); | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
const { awaitPageLoadAndIdle } = await import('../../idleCallback'); | ||
const promise = awaitPageLoadAndIdle(); | ||
|
||
// Simulate page load event | ||
setTimeout(() => { | ||
// @ts-expect-error test purpose | ||
document.readyState = 'complete'; | ||
window.dispatchEvent(new Event('load')); | ||
}, 20); | ||
|
||
await expect(promise).resolves.toBeUndefined(); | ||
expect(window.cancelIdleCallback).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
it('should resolve after the timeout if idle time is not sufficient', async () => { | ||
expect.assertions(2); | ||
|
||
// @ts-expect-error test purpose | ||
document.readyState = 'complete'; | ||
|
||
window.requestIdleCallback = jest.fn(callback => { | ||
const fakeDeadline = { timeRemaining: () => 10 } as IdleDeadline; | ||
return setTimeout(() => callback(fakeDeadline), 10) as unknown as number; | ||
}); | ||
window.cancelIdleCallback = jest.fn(id => { | ||
clearTimeout(id); | ||
}); | ||
await jest.isolateModulesAsync(async () => { | ||
const { awaitPageLoadAndIdle } = await import('../../idleCallback'); | ||
await expect( | ||
awaitPageLoadAndIdle({ remainingTime: 49, timeout: 1000 }) | ||
).resolves.toBeUndefined(); | ||
expect(window.cancelIdleCallback).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
13 changes: 13 additions & 0 deletions
13
packages/utils/src/__tests__/idleCallback/awaitPageLoadAndIdle.node.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/** | ||
* @jest-environment node | ||
*/ | ||
|
||
import { awaitPageLoadAndIdle } from '../../idleCallback'; | ||
|
||
describe('awaitPageLoadAndIdle', () => { | ||
it('should reject if called in a non-browser environment', async () => { | ||
await expect(awaitPageLoadAndIdle()).rejects.toThrow( | ||
'[awaitPageLoadAndIdle] server side is not supported' | ||
); | ||
}); | ||
}); |
96 changes: 96 additions & 0 deletions
96
packages/utils/src/__tests__/idleCallback/requestIdleCallback.browser.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
|
||
describe('browser requestIdleCallback and cancelIdleCallback polyfill', () => { | ||
let originalWindow: typeof window; | ||
|
||
beforeAll(() => { | ||
// Save the original window object | ||
originalWindow = global.window; | ||
}); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
// Mock window object for a browser environment | ||
global.window = {} as unknown as typeof originalWindow; | ||
}); | ||
|
||
afterEach(() => { | ||
// Clear all mocks and restore original window object after each test | ||
jest.clearAllMocks(); | ||
global.window = originalWindow; | ||
}); | ||
|
||
test('should use window.requestIdleCallback if available', async () => { | ||
expect.assertions(1); | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
global.window.requestIdleCallback = jest.fn(); | ||
const callback = jest.fn(); | ||
const { requestIdleCallback } = await import('../../idleCallback'); | ||
requestIdleCallback(callback); | ||
expect(global.window.requestIdleCallback).toHaveBeenCalledWith(callback); | ||
}); | ||
}); | ||
|
||
test('should fall back to polyfill if window.requestIdleCallback is unavailable', async () => { | ||
expect.assertions(3); | ||
|
||
// @ts-expect-error test purpose | ||
global.window.requestIdleCallback = undefined; | ||
expect(global.window.requestIdleCallback).toBeUndefined(); | ||
|
||
const callback = jest.fn(); | ||
const timeout = 3000; | ||
|
||
// Mock setTimeout to control its behavior | ||
jest.useFakeTimers(); | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
const { requestIdleCallback } = await import('../../idleCallback'); | ||
requestIdleCallback(callback); | ||
jest.advanceTimersByTime(timeout); | ||
expect(callback).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
didTimeout: false, | ||
timeRemaining: expect.any(Function) | ||
}) | ||
); | ||
// execute timeRemaining function | ||
expect(callback.mock.calls[0][0].timeRemaining()).toBe(50); | ||
|
||
jest.useRealTimers(); | ||
}); | ||
}); | ||
|
||
test('cancelIdleCallback should use window.cancelIdleCallback if available', async () => { | ||
global.window.cancelIdleCallback = jest.fn(); | ||
|
||
const id = 123; | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
const { cancelIdleCallback } = await import('../../idleCallback'); | ||
cancelIdleCallback(id); | ||
expect(global.window.cancelIdleCallback).toHaveBeenCalledWith(id); | ||
}); | ||
}); | ||
|
||
test('should fall back to clearTimeout if window.cancelIdleCallback is unavailable', async () => { | ||
expect.assertions(2); | ||
|
||
// @ts-expect-error test purpose | ||
global.window.cancelIdleCallback = undefined; | ||
expect(global.window.cancelIdleCallback).toBeUndefined(); | ||
|
||
// Mock clearTimeout to test the polyfill | ||
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); | ||
const id = setTimeout(() => {}, 0) as unknown as number; | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
const { cancelIdleCallback } = await import('../../idleCallback'); | ||
cancelIdleCallback(id); | ||
expect(clearTimeoutSpy).toHaveBeenCalledWith(id); | ||
}); | ||
}); | ||
}); |
43 changes: 43 additions & 0 deletions
43
packages/utils/src/__tests__/idleCallback/requestIdleCallback.node.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
* @jest-environment node | ||
*/ | ||
|
||
describe('node requestIdleCallback and cancelIdleCallback polyfill', () => { | ||
test('requestIdleCallback should invoke callback immediately on server', async () => { | ||
expect.assertions(3); | ||
expect(global.requestIdleCallback).toBeUndefined(); | ||
|
||
const callback = jest.fn(); | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
const { requestIdleCallback } = await import('../../idleCallback'); | ||
requestIdleCallback(callback); | ||
|
||
expect(callback).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
didTimeout: false, | ||
timeRemaining: expect.any(Function) | ||
}) | ||
); | ||
// execute timeRemaining function | ||
expect(callback.mock.calls[0][0].timeRemaining()).toBe(50); | ||
}); | ||
}); | ||
|
||
test('cancelIdleCallback should do nothing on server', async () => { | ||
expect.assertions(2); | ||
|
||
expect(global.cancelIdleCallback).toBeUndefined(); | ||
|
||
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); | ||
|
||
await jest.isolateModulesAsync(async () => { | ||
const { cancelIdleCallback } = await import('../../idleCallback'); | ||
|
||
cancelIdleCallback(123); | ||
|
||
// Should not call clearTimeout | ||
expect(clearTimeoutSpy).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.