Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.0] optimizations around prefetching page resources #1133

Merged
merged 29 commits into from
Jun 10, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c0ae4f9
Copy cache-dir files into site's .cache
KyleAMathews Jun 7, 2017
291b69c
Got new loader/componentrenderer working in development
KyleAMathews Jun 7, 2017
f92fdb9
Use ComponentRenderer for rendering
KyleAMathews Jun 7, 2017
f6c3112
On demand loading of async page resources working for prod builds
KyleAMathews Jun 7, 2017
d0ae0e6
Merge branch '1.0' into queue-requests
KyleAMathews Jun 7, 2017
0c8abd7
Use script loader so loading scripts don't block rendering
KyleAMathews Jun 7, 2017
69f60ae
Add gatsby-module-loader to replace bundle-loader to add ability to d…
KyleAMathews Jun 8, 2017
1f44c11
Split out loading/executing resources into functions
KyleAMathews Jun 8, 2017
f289c38
Add prefetcher
KyleAMathews Jun 8, 2017
389f7e9
changing order to see if it fixes not removing event handler
KyleAMathews Jun 8, 2017
b0f2c88
Just don't have a loader for now
KyleAMathews Jun 8, 2017
d4a841b
Updates to service worker plugin
KyleAMathews Jun 9, 2017
f2f368b
Add tests for find-page + make it work with prefixed links
KyleAMathews Jun 9, 2017
9ae6a60
Proritize links that mount first i.e. are higher on the page
KyleAMathews Jun 9, 2017
7766b77
Wait for all links to mount before fetching so resource count is correct
KyleAMathews Jun 9, 2017
ab5da7f
Add plop template for adding new example sites
KyleAMathews Jun 10, 2017
c7dd9b1
Add plugin for nprogress to auto show when page loading is delayed
KyleAMathews Jun 10, 2017
4644ea7
Don't break layout if there's no content
KyleAMathews Jun 10, 2017
aeb9a77
Log at end of build how long build took
KyleAMathews Jun 10, 2017
8162a91
Not using this package
KyleAMathews Jun 10, 2017
bbf5302
Merge remote-tracking branch 'origin/1.0' into queue-requests
KyleAMathews Jun 10, 2017
19f7c72
Remove logging + run format
KyleAMathews Jun 10, 2017
a1fd507
Add the nprogress plugin
KyleAMathews Jun 10, 2017
ce5ca59
Fix some emitter code
KyleAMathews Jun 10, 2017
ab0e3f4
Make it easy to override the default color of the nprogress bar
KyleAMathews Jun 10, 2017
33e9e8e
Fix check on variable
KyleAMathews Jun 10, 2017
c6b486a
Fix prefetching
KyleAMathews Jun 10, 2017
6cac6e1
Fix test
KyleAMathews Jun 10, 2017
fd01c58
Add nprogress plugin to list
KyleAMathews Jun 10, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add prefetcher
  • Loading branch information
KyleAMathews committed Jun 8, 2017
commit f289c388cf877b6f5dd172d8a952464df05c07d7
64 changes: 64 additions & 0 deletions packages/gatsby/src/cache-dir/__tests__/prefetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
let prefetcher
let createResourceDownload

describe(`Loader`, () => {
beforeEach(() => {
createResourceDownload = jest.fn()
prefetcher = require(`../prefetcher.js`)({
getNextQueuedResources: () => Math.random(),
createResourceDownload,
})
prefetcher.empty()
})

test(`It tracks when pages are loading`, () => {
prefetcher.onPreLoadPageResources()
prefetcher.onPreLoadPageResources()
expect(prefetcher.getState().pagesLoading).toEqual(2)
})

test(`It tracks when pages finish loading`, () => {
prefetcher.onPreLoadPageResources()
prefetcher.onPostLoadPageResources()
expect(prefetcher.getState().pagesLoading).toEqual(0)
})

test(`When a new resource is queued, it tries to download it`, () => {
prefetcher.onNewResourcesAdded()
expect(createResourceDownload.mock.calls.length).toBe(1)
})

test(`If page resources are downloading, it doesn't prefetch until after it's finished`, () => {
prefetcher.onPreLoadPageResources()
prefetcher.onNewResourcesAdded()
expect(createResourceDownload.mock.calls.length).toBe(0)
prefetcher.onPostLoadPageResources()
expect(createResourceDownload.mock.calls.length).toBe(1)
})

test(`Once one resource finishes downloading, it starts another`, () => {
prefetcher.onNewResourcesAdded()
// Get resource name
const resourceName = createResourceDownload.mock.calls[0][0]

// New resources added shouldn't trigger new downloads.
prefetcher.onNewResourcesAdded()
prefetcher.onNewResourcesAdded()
expect(createResourceDownload.mock.calls.length).toBe(1)

// Finish the first download triggers another download.
prefetcher.onResourcedFinished(resourceName)
expect(createResourceDownload.mock.calls.length).toBe(2)
})

test(`It stops downloading when the resourcesArray is empty`, () => {
prefetcher = require(`../prefetcher.js`)({
getNextQueuedResources: () => undefined,
createResourceDownload: jest.fn(),
})
prefetcher.empty()

prefetcher.onNewResourcesAdded()
expect(createResourceDownload.mock.calls.length).toBe(0)
})
})
6 changes: 2 additions & 4 deletions packages/gatsby/src/cache-dir/component-renderer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { createElement } from "react"
import loader from "./loader"
import emitter from "./emitter"

// Pass pathname in as prop.
// component will try fetching resources. If they exist,
Expand All @@ -24,17 +25,14 @@ class ComponentRenderer extends React.Component {

componentDidMount() {
// listen to events.
___emitter.on(`onPostLoadPageResources`, e => {
console.log("onPostLoadPageResources", e)
emitter.on(`onPostLoadPageResources`, e => {
if (e.page.path === this.props.location.pathname) {
this.setState({ pageResources: e.pageResources })
}
})
}

render() {
console.log("rendering ComponentRenderer")
console.log(this)
if (this.state.pageResources) {
return createElement(this.state.pageResources.component, {
...this.props,
Expand Down
3 changes: 3 additions & 0 deletions packages/gatsby/src/cache-dir/emitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import mitt from "mitt"
const emitter = mitt()
module.exports = emitter
92 changes: 51 additions & 41 deletions packages/gatsby/src/cache-dir/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ if (typeof window !== `undefined`) {
}

import pageFinderFactor from "./find-page"
import emitter from "./emitter"
let findPage

let syncRequires = {}
Expand All @@ -20,6 +21,30 @@ let resourcesArray = []
let resourcesCount = {}
const preferDefault = m => (m && m.default) || m

// Prefetcher logic
const prefetcher = require(`./prefetcher`)({
getNextQueuedResources: () => resourcesArray.slice(-1)[0],
createResourceDownload: resourceName => {
console.log("fetching resource:", resourceName)
console.log(`resourcesArray length`, resourcesArray.length)
fetchResource(resourceName, () => {
resourcesArray = resourcesArray.filter(r => r !== resourceName)
console.log(`resourcesArray length`, resourcesArray.length)
prefetcher.onResourcedFinished(resourceName)
console.log(`resourceStrCache`, resourceStrCache)
console.log(`resourceCache`, resourceCache)
})
},
})
emitter.on(`ON_PRE_LOAD_PAGE_RESOURCES`, e => {
console.log(`ON_PRE_LOAD_PAGE_RESOURCES`, e)
prefetcher.onPreLoadPageResources(e)
})
emitter.on(`ON_POST_LOAD_PAGE_RESOURCES`, e => {
console.log(`ON_POST_LOAD_PAGE_RESOURCES`, e)
prefetcher.onPostLoadPageResources(e)
})

const sortResourcesByCount = (a, b) => {
if (resourcesCount[a] > resourcesCount[b]) {
return 1
Expand All @@ -42,7 +67,7 @@ const sortPagesByCount = (a, b) => {

const fetchResource = (resourceName, cb = () => {}) => {
if (resourceStrCache[resourceName]) {
return resourceStrCache[resourceName]
return cb(null, resourceStrCache[resourceName])
} else {
// Find resource
const resourceFunction = resourceName.slice(0, 6) === `page-c`
Expand All @@ -60,6 +85,7 @@ const fetchResource = (resourceName, cb = () => {}) => {
}

const getResourceModule = (resourceName, cb) => {
console.log(`getting resourceName`, resourceName)
if (resourceCache[resourceName]) {
return cb(null, resourceCache[resourceName])
} else {
Expand All @@ -77,39 +103,6 @@ const getResourceModule = (resourceName, cb) => {
}
}

// TODO split this into two functions, one for fetching
// resource and the other for executing it.
// This prefetcher just sits around waiting for
// new resources to be added. When hears onPreLoadPageResources
// it stops until it hears onPostLoadPageResources
// actually, make it a state machine like thing that's listening
// to events and flipping it's state around.
//
// This is stupid complex. Need tests somehow.
// const prefetcher = () => {
// // Get top resource and start downloading it.
// const nextResource = resourcesArray.slice(-1)[0]
// if (nextResource) {
// console.log("prefetching next resource", nextResource)
// fetchResource(nextResource, (err, executeChunk) => {
// console.log("executeChunk", executeChunk)
// resourcesArray = resourcesArray.filter(r => r !== nextResource)
// setTimeout(() => {
// console.time(`execute resource`)
// const module = getResourceModule(nextResource)
// console.timeEnd(`execute resource`)
// console.log("module", module)
// prefetcher()
// }, 500)
// })
// } else {
// // Wait a second and try again
// setTimeout(() => prefetcher(), 1000)
// }
// }

// setTimeout(() => prefetcher(), 2500)

const queue = {
empty: () => {
pathArray = []
Expand Down Expand Up @@ -159,7 +152,12 @@ const queue = {
resourcesCount[page.jsonName] += 1
}

if (resourcesArray.indexOf(page.jsonName) === -1) {
// Before adding checking that the JSON resource isn't either
// already queued or been downloading.
if (
resourcesArray.indexOf(page.jsonName) === -1 &&
!resourceStrCache[page.jsonName]
) {
resourcesArray.unshift(page.jsonName)
}
}
Expand All @@ -170,13 +168,19 @@ const queue = {
resourcesCount[page.componentChunkName] += 1
}

if (resourcesArray.indexOf(page.componentChunkName) === -1) {
// Before adding checking that the component resource isn't either
// already queued or been downloading.
if (
resourcesArray.indexOf(page.componentChunkName) === -1 &&
!resourceStrCache[page.jsonName]
) {
resourcesArray.unshift(page.componentChunkName)
}
}

// Sort resources by resourcesCount
// Sort resources by resourcesCount.
resourcesArray.sort(sortResourcesByCount)
prefetcher.onNewResourcesAdded()

return true
},
Expand All @@ -195,7 +199,7 @@ const queue = {
hasPage: pathname => findPage(pathname),
has: path => pathArray.some(p => p === path),
getResourcesForPathname: path => {
___emitter.emit(`onPreLoadPageResources`, { path })
emitter.emit(`onPreLoadPageResources`, { path })
// In development we know the code is loaded already
// so we just return with it immediately.
if (process.env.NODE_ENV !== `production`) {
Expand All @@ -204,7 +208,8 @@ const queue = {
component: syncRequires.components[page.componentChunkName],
json: syncRequires.json[page.jsonName],
}
___emitter.emit(`onPostLoadPageResources`, { page, pageResources })
console.log(`EMITTING onPostLoadPageResources`, page, pageResources)
emitter.emit(`onPostLoadPageResources`, { page, pageResources })
return pageResources
} else {
// Check if it's in the cache already.
Expand All @@ -218,24 +223,29 @@ const queue = {
let json
const done = () => {
if (component && json) {
console.log("done loading")
pathScriptsCache[path] = { component, json }
___emitter.emit(`onPostLoadPageResources`, {
const pageResources = { component, json }
console.log(`EMITTING onPostLoadPageResources`, page, pageResources)
emitter.emit(`onPostLoadPageResources`, {
page,
pageResources: { component, json },
pageResources,
})
}
}
getResourceModule(page.componentChunkName, (err, c) => {
if (err) {
return console.log("we failed folks")
}
console.log("done getting component")
component = c
done()
})
getResourceModule(page.jsonName, (err, j) => {
if (err) {
return console.log("we failed folks")
}
console.log("done getting json")
json = j
done()
})
Expand Down
69 changes: 69 additions & 0 deletions packages/gatsby/src/cache-dir/prefetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module.exports = ({ getNextQueuedResources, createResourceDownload }) => {
let pagesLoading = 0
let resourcesDownloading = []

// Do things
const startResourceDownloading = () => {
const nextResource = getNextQueuedResources()
if (nextResource) {
resourcesDownloading.push(nextResource)
createResourceDownload(nextResource)
console.log({ pagesLoading, resourcesDownloading })
}
}

const reducer = action => {
switch (action.type) {
case `RESOURCE_FINISHED`:
console.log(`reducer RESOURCE_FINISHED`, action, resourcesDownloading)
resourcesDownloading = resourcesDownloading.filter(
r => r !== action.payload
)
break
case `ON_PRE_LOAD_PAGE_RESOURCES`:
pagesLoading += 1
break
case `ON_POST_LOAD_PAGE_RESOURCES`:
pagesLoading -= 1
break
case `ON_NEW_RESOURCES_ADDED`:
break
}

// Take actions.
if (resourcesDownloading.length === 0 && pagesLoading === 0) {
// Start another resource downloading.
startResourceDownloading()
}
}

return {
onResourcedFinished: event => {
// Tell prefetcher that the resource finished downloading
// so it can grab the next one.
reducer({ type: `RESOURCE_FINISHED`, payload: event })
},
onPreLoadPageResources: event => {
// Tell prefetcher a page load has started so it should stop
// loading anything new
reducer({ type: `ON_PRE_LOAD_PAGE_RESOURCES`, payload: event })
},
onPostLoadPageResources: event => {
// Tell prefetcher a page load has finished so it should start
// loading resources again.
reducer({ type: `ON_POST_LOAD_PAGE_RESOURCES`, payload: event })
},
onNewResourcesAdded: () => {
// Tell prefetcher that more resources to be downloaded have
// been added.
reducer({ type: `ON_NEW_RESOURCES_ADDED` })
},
getState: () => {
return { pagesLoading, resourcesDownloading }
},
empty: () => {
pagesLoading = 0
resourcesDownloading = []
},
}
}
Loading