Skip to content

Commit 2975c4d

Browse files
authoredOct 18, 2021
feat(gatsby,gatsby-link): add queue to prefetch (#33530)
1 parent 68fe836 commit 2975c4d

File tree

7 files changed

+143
-81
lines changed

7 files changed

+143
-81
lines changed
 

‎packages/gatsby-legacy-polyfills/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"dist/"
3232
],
3333
"devDependencies": {
34+
"yet-another-abortcontroller-polyfill": "0.0.4",
3435
"chokidar-cli": "^3.0.0",
3536
"codegen.macro": "^4.1.0",
3637
"core-js": "3.9.0",

‎packages/gatsby-legacy-polyfills/src/polyfills.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ codegen`
66
module.exports = imports.map(file => 'import "core-js/' + file + '"').join("\\n")
77
`
88

9+
import "yet-another-abortcontroller-polyfill"
910
import "whatwg-fetch"
1011
import "url-polyfill"
1112
import assign from "object-assign"

‎packages/gatsby-link/src/index.js

+22-22
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,14 @@ const createIntersectionObserver = (el, cb) => {
8080
if (el === entry.target) {
8181
// Check if element is within viewport, remove listener, destroy observer, and run link callback.
8282
// MSEdge doesn't currently support isIntersecting, so also test for an intersectionRatio > 0
83-
if (entry.isIntersecting || entry.intersectionRatio > 0) {
84-
io.unobserve(el)
85-
io.disconnect()
86-
cb()
87-
}
83+
cb(entry.isIntersecting || entry.intersectionRatio > 0)
8884
}
8985
})
9086
})
87+
9188
// Add element to the observer
9289
io.observe(el)
90+
9391
return { instance: io, el }
9492
}
9593

@@ -113,6 +111,7 @@ class GatsbyLink extends React.Component {
113111
this.state = {
114112
IOSupported,
115113
}
114+
this.abortPrefetch = null
116115
this.handleRef = this.handleRef.bind(this)
117116
}
118117

@@ -132,22 +131,10 @@ class GatsbyLink extends React.Component {
132131
// Prefetch is used to speed up next navigations. When you use it on the current navigation,
133132
// there could be a race-condition where Chrome uses the stale data instead of waiting for the network to complete
134133
if (currentPath !== newPathName) {
135-
___loader.enqueue(newPathName)
136-
}
137-
}
138-
139-
componentDidUpdate(prevProps, prevState) {
140-
// Preserve non IO functionality if no support
141-
if (this.props.to !== prevProps.to && !this.state.IOSupported) {
142-
this._prefetch()
134+
return ___loader.enqueue(newPathName)
143135
}
144-
}
145136

146-
componentDidMount() {
147-
// Preserve non IO functionality if no support
148-
if (!this.state.IOSupported) {
149-
this._prefetch()
150-
}
137+
return undefined
151138
}
152139

153140
componentWillUnmount() {
@@ -156,21 +143,34 @@ class GatsbyLink extends React.Component {
156143
}
157144
const { instance, el } = this.io
158145

146+
if (this.abortPrefetch) {
147+
this.abortPrefetch.abort()
148+
}
149+
159150
instance.unobserve(el)
160151
instance.disconnect()
161152
}
162153

163154
handleRef(ref) {
164-
if (this.props.innerRef && this.props.innerRef.hasOwnProperty(`current`)) {
155+
if (
156+
this.props.innerRef &&
157+
Object.prototype.hasOwnProperty.call(this.props.innerRef, `current`)
158+
) {
165159
this.props.innerRef.current = ref
166160
} else if (this.props.innerRef) {
167161
this.props.innerRef(ref)
168162
}
169163

170164
if (this.state.IOSupported && ref) {
171165
// If IO supported and element reference found, setup Observer functionality
172-
this.io = createIntersectionObserver(ref, () => {
173-
this._prefetch()
166+
this.io = createIntersectionObserver(ref, inViewPort => {
167+
if (inViewPort) {
168+
this.abortPrefetch = this._prefetch()
169+
} else {
170+
if (this.abortPrefetch) {
171+
this.abortPrefetch.abort()
172+
}
173+
}
174174
})
175175
}
176176
}

‎packages/gatsby/cache-dir/__tests__/dev-loader.js

+16-33
Original file line numberDiff line numberDiff line change
@@ -482,26 +482,32 @@ describe(`Dev loader`, () => {
482482
describe(`prefetch`, () => {
483483
const flushPromises = () => new Promise(resolve => setImmediate(resolve))
484484

485-
it(`shouldn't prefetch when shouldPrefetch is false`, () => {
486-
const devLoader = new DevLoader(asyncRequires, [])
485+
it(`shouldn't prefetch when shouldPrefetch is false`, async () => {
486+
jest.useFakeTimers()
487+
const devLoader = new DevLoader(null, [])
487488
devLoader.shouldPrefetch = jest.fn(() => false)
488489
devLoader.doPrefetch = jest.fn()
489490
devLoader.apiRunner = jest.fn()
491+
const prefetchPromise = devLoader.prefetch(`/mypath/`)
492+
jest.runAllTimers()
490493

491-
expect(devLoader.prefetch(`/mypath/`)).toBe(false)
494+
expect(await prefetchPromise).toBe(false)
492495
expect(devLoader.shouldPrefetch).toHaveBeenCalledWith(`/mypath/`)
493496
expect(devLoader.apiRunner).not.toHaveBeenCalled()
494497
expect(devLoader.doPrefetch).not.toHaveBeenCalled()
495498
})
496499

497-
it(`should trigger custom prefetch logic when core is disabled`, () => {
498-
const devLoader = new DevLoader(asyncRequires, [])
500+
it(`should trigger custom prefetch logic when core is disabled`, async () => {
501+
jest.useFakeTimers()
502+
const devLoader = new DevLoader(null, [])
499503
devLoader.shouldPrefetch = jest.fn(() => true)
500504
devLoader.doPrefetch = jest.fn()
501505
devLoader.apiRunner = jest.fn()
502506
devLoader.prefetchDisabled = true
503507

504-
expect(devLoader.prefetch(`/mypath/`)).toBe(false)
508+
const prefetchPromise = devLoader.prefetch(`/mypath/`)
509+
jest.runAllTimers()
510+
expect(await prefetchPromise).toBe(false)
505511
expect(devLoader.shouldPrefetch).toHaveBeenCalledWith(`/mypath/`)
506512
expect(devLoader.apiRunner).toHaveBeenCalledWith(`onPrefetchPathname`, {
507513
pathname: `/mypath/`,
@@ -511,12 +517,14 @@ describe(`Dev loader`, () => {
511517

512518
it(`should prefetch when not yet triggered`, async () => {
513519
jest.useFakeTimers()
514-
const devLoader = new DevLoader(asyncRequires, [])
520+
const devLoader = new DevLoader(null, [])
515521
devLoader.shouldPrefetch = jest.fn(() => true)
516522
devLoader.apiRunner = jest.fn()
517523
devLoader.doPrefetch = jest.fn(() => Promise.resolve({}))
524+
const prefetchPromise = devLoader.prefetch(`/mypath/`)
525+
jest.runAllTimers()
518526

519-
expect(devLoader.prefetch(`/mypath/`)).toBe(true)
527+
expect(await prefetchPromise).toBe(true)
520528

521529
// wait for doPrefetchPromise
522530
await flushPromises()
@@ -532,30 +540,5 @@ describe(`Dev loader`, () => {
532540
}
533541
)
534542
})
535-
536-
it(`should only run apis once`, async () => {
537-
const devLoader = new DevLoader(asyncRequires, [])
538-
devLoader.shouldPrefetch = jest.fn(() => true)
539-
devLoader.apiRunner = jest.fn()
540-
devLoader.doPrefetch = jest.fn(() => Promise.resolve({}))
541-
542-
expect(devLoader.prefetch(`/mypath/`)).toBe(true)
543-
expect(devLoader.prefetch(`/mypath/`)).toBe(true)
544-
545-
// wait for doPrefetchPromise
546-
await flushPromises()
547-
548-
expect(devLoader.apiRunner).toHaveBeenCalledTimes(2)
549-
expect(devLoader.apiRunner).toHaveBeenNthCalledWith(
550-
1,
551-
`onPrefetchPathname`,
552-
expect.anything()
553-
)
554-
expect(devLoader.apiRunner).toHaveBeenNthCalledWith(
555-
2,
556-
`onPostPrefetchPathname`,
557-
expect.anything()
558-
)
559-
})
560543
})
561544
})

‎packages/gatsby/cache-dir/__tests__/loader.js

+19-7
Original file line numberDiff line numberDiff line change
@@ -516,26 +516,32 @@ describe(`Production loader`, () => {
516516
describe(`prefetch`, () => {
517517
const flushPromises = () => new Promise(resolve => setImmediate(resolve))
518518

519-
it(`shouldn't prefetch when shouldPrefetch is false`, () => {
519+
it(`shouldn't prefetch when shouldPrefetch is false`, async () => {
520+
jest.useFakeTimers()
520521
const prodLoader = new ProdLoader(null, [])
521522
prodLoader.shouldPrefetch = jest.fn(() => false)
522523
prodLoader.doPrefetch = jest.fn()
523524
prodLoader.apiRunner = jest.fn()
525+
const prefetchPromise = prodLoader.prefetch(`/mypath/`)
526+
jest.runAllTimers()
524527

525-
expect(prodLoader.prefetch(`/mypath/`)).toBe(false)
528+
expect(await prefetchPromise).toBe(false)
526529
expect(prodLoader.shouldPrefetch).toHaveBeenCalledWith(`/mypath/`)
527530
expect(prodLoader.apiRunner).not.toHaveBeenCalled()
528531
expect(prodLoader.doPrefetch).not.toHaveBeenCalled()
529532
})
530533

531-
it(`should trigger custom prefetch logic when core is disabled`, () => {
534+
it(`should trigger custom prefetch logic when core is disabled`, async () => {
535+
jest.useFakeTimers()
532536
const prodLoader = new ProdLoader(null, [])
533537
prodLoader.shouldPrefetch = jest.fn(() => true)
534538
prodLoader.doPrefetch = jest.fn()
535539
prodLoader.apiRunner = jest.fn()
536540
prodLoader.prefetchDisabled = true
537541

538-
expect(prodLoader.prefetch(`/mypath/`)).toBe(false)
542+
const prefetchPromise = prodLoader.prefetch(`/mypath/`)
543+
jest.runAllTimers()
544+
expect(await prefetchPromise).toBe(false)
539545
expect(prodLoader.shouldPrefetch).toHaveBeenCalledWith(`/mypath/`)
540546
expect(prodLoader.apiRunner).toHaveBeenCalledWith(`onPrefetchPathname`, {
541547
pathname: `/mypath/`,
@@ -549,8 +555,10 @@ describe(`Production loader`, () => {
549555
prodLoader.shouldPrefetch = jest.fn(() => true)
550556
prodLoader.apiRunner = jest.fn()
551557
prodLoader.doPrefetch = jest.fn(() => Promise.resolve({}))
558+
const prefetchPromise = prodLoader.prefetch(`/mypath/`)
559+
jest.runAllTimers()
552560

553-
expect(prodLoader.prefetch(`/mypath/`)).toBe(true)
561+
expect(await prefetchPromise).toBe(true)
554562

555563
// wait for doPrefetchPromise
556564
await flushPromises()
@@ -568,13 +576,17 @@ describe(`Production loader`, () => {
568576
})
569577

570578
it(`should only run apis once`, async () => {
579+
jest.useFakeTimers()
571580
const prodLoader = new ProdLoader(null, [])
572581
prodLoader.shouldPrefetch = jest.fn(() => true)
573582
prodLoader.apiRunner = jest.fn()
574583
prodLoader.doPrefetch = jest.fn(() => Promise.resolve({}))
584+
const prefetchPromise = prodLoader.prefetch(`/mypath/`)
585+
const prefetchPromise2 = prodLoader.prefetch(`/mypath/`)
586+
jest.runAllTimers()
575587

576-
expect(prodLoader.prefetch(`/mypath/`)).toBe(true)
577-
expect(prodLoader.prefetch(`/mypath/`)).toBe(true)
588+
expect(await prefetchPromise).toBe(true)
589+
expect(await prefetchPromise2).toBe(true)
578590

579591
// wait for doPrefetchPromise
580592
await flushPromises()

‎packages/gatsby/cache-dir/loader.js

+79-19
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const createPageDataUrl = rawPath => {
3333
}
3434

3535
function doFetch(url, method = `GET`) {
36-
return new Promise((resolve, reject) => {
36+
return new Promise(resolve => {
3737
const req = new XMLHttpRequest()
3838
req.open(method, url, true)
3939
req.onreadystatechange = () => {
@@ -98,6 +98,8 @@ export class BaseLoader {
9898
this.inFlightDb = new Map()
9999
this.staticQueryDb = {}
100100
this.pageDataDb = new Map()
101+
this.isPrefetchQueueRunning = false
102+
this.prefetchQueued = []
101103
this.prefetchTriggered = new Set()
102104
this.prefetchCompleted = new Set()
103105
this.loadComponent = loadComponent
@@ -396,32 +398,90 @@ export class BaseLoader {
396398

397399
prefetch(pagePath) {
398400
if (!this.shouldPrefetch(pagePath)) {
399-
return false
401+
return {
402+
then: resolve => resolve(false),
403+
abort: () => {},
404+
}
405+
}
406+
if (this.prefetchTriggered.has(pagePath)) {
407+
return {
408+
then: resolve => resolve(true),
409+
abort: () => {},
410+
}
411+
}
412+
413+
const defer = {
414+
resolve: null,
415+
reject: null,
416+
promise: null,
400417
}
418+
defer.promise = new Promise((resolve, reject) => {
419+
defer.resolve = resolve
420+
defer.reject = reject
421+
})
422+
this.prefetchQueued.push([pagePath, defer])
423+
const abortC = new AbortController()
424+
abortC.signal.addEventListener(`abort`, () => {
425+
const index = this.prefetchQueued.findIndex(([p]) => p === pagePath)
426+
// remove from the queue
427+
if (index !== -1) {
428+
this.prefetchQueued.splice(index, 1)
429+
}
430+
})
401431

402-
// Tell plugins with custom prefetching logic that they should start
403-
// prefetching this path.
404-
if (!this.prefetchTriggered.has(pagePath)) {
405-
this.apiRunner(`onPrefetchPathname`, { pathname: pagePath })
406-
this.prefetchTriggered.add(pagePath)
432+
if (!this.isPrefetchQueueRunning) {
433+
this.isPrefetchQueueRunning = true
434+
setTimeout(() => {
435+
this._processNextPrefetchBatch()
436+
}, 3000)
407437
}
408438

409-
// If a plugin has disabled core prefetching, stop now.
410-
if (this.prefetchDisabled) {
411-
return false
439+
return {
440+
then: (resolve, reject) => defer.promise.then(resolve, reject),
441+
abort: abortC.abort.bind(abortC),
412442
}
443+
}
444+
445+
_processNextPrefetchBatch() {
446+
const idleCallback = window.requestIdleCallback || (cb => setTimeout(cb, 0))
447+
448+
idleCallback(() => {
449+
const toPrefetch = this.prefetchQueued.splice(0, 4)
450+
const prefetches = Promise.all(
451+
toPrefetch.map(([pagePath, dPromise]) => {
452+
// Tell plugins with custom prefetching logic that they should start
453+
// prefetching this path.
454+
if (!this.prefetchTriggered.has(pagePath)) {
455+
this.apiRunner(`onPrefetchPathname`, { pathname: pagePath })
456+
this.prefetchTriggered.add(pagePath)
457+
}
458+
459+
// If a plugin has disabled core prefetching, stop now.
460+
if (this.prefetchDisabled) {
461+
return dPromise.resolve(false)
462+
}
463+
464+
return this.doPrefetch(findPath(pagePath)).then(() => {
465+
if (!this.prefetchCompleted.has(pagePath)) {
466+
this.apiRunner(`onPostPrefetchPathname`, { pathname: pagePath })
467+
this.prefetchCompleted.add(pagePath)
468+
}
469+
470+
dPromise.resolve(true)
471+
})
472+
})
473+
)
413474

414-
const realPath = findPath(pagePath)
415-
// Todo make doPrefetch logic cacheable
416-
// eslint-disable-next-line consistent-return
417-
this.doPrefetch(realPath).then(() => {
418-
if (!this.prefetchCompleted.has(pagePath)) {
419-
this.apiRunner(`onPostPrefetchPathname`, { pathname: pagePath })
420-
this.prefetchCompleted.add(pagePath)
475+
if (this.prefetchQueued.length) {
476+
prefetches.then(() => {
477+
setTimeout(() => {
478+
this._processNextPrefetchBatch()
479+
}, 3000)
480+
})
481+
} else {
482+
this.isPrefetchQueueRunning = false
421483
}
422484
})
423-
424-
return true
425485
}
426486

427487
doPrefetch(pagePath) {

0 commit comments

Comments
 (0)
Please sign in to comment.