Skip to content

Commit

Permalink
Adjust SSG Loading Behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
Timer committed Feb 12, 2020
1 parent 04d5bbc commit 40d64ea
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 118 deletions.
11 changes: 7 additions & 4 deletions packages/next/build/index.ts
Expand Up @@ -657,15 +657,18 @@ export default async function build(dir: string, conf = null): Promise<void> {
// n.b. we cannot handle this above in combinedPages because the dynamic
// page must be in the `pages` array, but not in the mapping.
exportPathMap: (defaultMap: any) => {
// Generate fallback for dynamically routed pages to use as
// the loading state for pages while the data is being populated
// Dynamically routed pages should be prerendered to be used as
// a client-side skeleton (fallback) while data is being fetched.
// This ensures the end-user never sees a 500 or slow response from the
// server.
//
// Note: prerendering disables automatic static optimization.
ssgPages.forEach(page => {
if (isDynamicRoute(page)) {
tbdPrerenderRoutes.push(page)
// set __nextFallback query so render doesn't call
// getStaticProps/getServerProps

// Override the rendering for the dynamic page to be treated as a
// fallback render.
defaultMap[page] = { page, query: { __nextFallback: true } }
}
})
Expand Down
11 changes: 8 additions & 3 deletions packages/next/client/index.js
Expand Up @@ -97,9 +97,10 @@ class Container extends React.Component {
})
}

// If page was exported and has a querystring
// If it's a dynamic route or has a querystring
// if it's a fallback page
// We need to replace the router state if:
// - the page was (auto) exported and has a query string or search (hash)
// - it was auto exported and is a dynamic route (to provide params)
// - if it is a client-side skeleton (fallback render)
if (
router.isSsr &&
(isFallback ||
Expand All @@ -121,6 +122,10 @@ class Container extends React.Component {
// client-side hydration. Your app should _never_ use this property.
// It may change at any time without notice.
_h: 1,
// Fallback pages must trigger the data fetch, so the transition is
// not shallow.
// Other pages (strictly updating query) happens shallowly, as data
// requirements would already be present.
shallow: !isFallback,
}
)
Expand Down
136 changes: 39 additions & 97 deletions packages/next/next-server/lib/router/router.ts
Expand Up @@ -78,15 +78,17 @@ const fetchNextData = (
)
.then(res => {
if (!res.ok) {
const error = new Error(`Failed to load static props`)
;(error as any).statusCode = res.status
throw error
throw new Error(`Failed to load static props`)
}
return res.json()
})
.then(data => {
return cb ? cb(data) : data
})
.catch((err: Error) => {
;(err as any).code = 'PAGE_LOAD_ERROR'
throw err
})
}

export default class Router implements BaseRouter {
Expand Down Expand Up @@ -391,65 +393,31 @@ export default class Router implements BaseRouter {

// If shallow is true and the route exists in the router cache we reuse the previous result
this.getRouteInfo(route, pathname, query, as, shallow).then(routeInfo => {
let emitHistory = false

const doRouteChange = (routeInfo: RouteInfo, complete: boolean) => {
const { error } = routeInfo

if (error && error.cancelled) {
return resolve(false)
}
const { error } = routeInfo

if (!emitHistory) {
emitHistory = true
Router.events.emit('beforeHistoryChange', as)
this.changeState(method, url, addBasePath(as), options)
}
if (error && error.cancelled) {
return resolve(false)
}

if (process.env.NODE_ENV !== 'production') {
const appComp: any = this.components['/_app'].Component
;(window as any).next.isPrerendered =
appComp.getInitialProps === appComp.origGetInitialProps &&
!(routeInfo.Component as any).getInitialProps
}
Router.events.emit('beforeHistoryChange', as)
this.changeState(method, url, addBasePath(as), options)

this.set(route, pathname, query, as, routeInfo)
if (process.env.NODE_ENV !== 'production') {
const appComp: any = this.components['/_app'].Component
;(window as any).next.isPrerendered =
appComp.getInitialProps === appComp.origGetInitialProps &&
!(routeInfo.Component as any).getInitialProps
}

if (complete) {
if (error) {
Router.events.emit('routeChangeError', error, as)
throw error
}
this.set(route, pathname, query, as, routeInfo)

Router.events.emit('routeChangeComplete', as)
resolve(true)
}
if (error) {
Router.events.emit('routeChangeError', error, as)
throw error
}

if ((routeInfo as any).dataRes) {
const dataRes = (routeInfo as any).dataRes as Promise<RouteInfo>

// to prevent a flash of the fallback page we delay showing it for
// 110ms and race the timeout with the data response. If the data
// beats the timeout we skip showing the fallback
Promise.race([
new Promise(resolve => setTimeout(() => resolve(false), 110)),
dataRes,
])
.then((data: any) => {
if (!data) {
// data didn't win the race, show fallback
doRouteChange(routeInfo, false)
}
return dataRes
})
.then(finalData => {
// render with the data and complete route change
doRouteChange(finalData as RouteInfo, true)
}, reject)
} else {
doRouteChange(routeInfo, true)
}
Router.events.emit('routeChangeComplete', as)
return resolve(true)
}, reject)
})
}
Expand Down Expand Up @@ -518,51 +486,25 @@ export default class Router implements BaseRouter {
}
}

const isSSG = (Component as any).__N_SSG
const isSSP = (Component as any).__N_SSP

const handleData = (props: any) => {
return this._getData<RouteInfo>(() =>
(Component as any).__N_SSG
? this._getStaticData(as)
: (Component as any).__N_SSP
? this._getServerData(as)
: this.getInitialProps(
Component,
// we provide AppTree later so this needs to be `any`
{
pathname,
query,
asPath: as,
} as any
)
).then(props => {
routeInfo.props = props
this.components[route] = routeInfo
return routeInfo
}

// resolve with fallback routeInfo and promise for data
if (isSSG || isSSP) {
const dataMethod = () =>
isSSG ? this._getStaticData(as) : this._getServerData(as)

const retry = (error: Error & { statusCode: number }) => {
if (error.statusCode === 404) {
throw error
}
return dataMethod()
}

return Promise.resolve({
...routeInfo,
props: {},
dataRes: this._getData(() =>
dataMethod()
// we retry for data twice unless we get a 404
.catch(retry)
.catch(retry)
.then((props: any) => handleData(props))
),
})
}

return this._getData<RouteInfo>(() =>
this.getInitialProps(
Component,
// we provide AppTree later so this needs to be `any`
{
pathname,
query,
asPath: as,
} as any
)
).then(props => handleData(props))
})
})
.catch(err => {
return new Promise(resolve => {
Expand Down
40 changes: 26 additions & 14 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -992,22 +992,34 @@ export default class Server {
return { html, pageData, sprRevalidate }
})

// render fallback if for a preview path or a non-seeded dynamic path
const isProduction = !this.renderOpts.dev
const isDynamicPathname = isDynamicRoute(pathname)
if (
!isResSent(res) &&
!isDataReq &&
((isPreviewMode &&
// A header can opt into the blocking behavior.
req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking') ||
isDynamicPathname)
) {
let html = ''

const isProduction = !this.renderOpts.dev
if (isProduction && (isDynamicPathname || !isPreviewMode)) {
const didRespond = isResSent(res)
// const isForcedBlocking =
// req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking'

// When we did not respond from cache, we need to choose to block on
// rendering or return a skeleton.
//
// * Data requests always block.
//
// * Preview mode toggles all pages to be resolved in a blocking manner.
//
// * Non-dynamic pages should block (though this is an be an impossible
// case in production).
//
// * Dynamic pages should return their skeleton, then finish the data
// request on the client-side.
//
if (!didRespond && !isDataReq && !isPreviewMode && isDynamicPathname) {
let html: string

// Production already emitted the fallback as static HTML.
if (isProduction) {
html = await getFallback(pathname)
} else {
}
// We need to generate the fallback on-demand for development.
else {
query.__nextFallback = 'true'
if (isLikeServerless) {
this.prepareServerlessUrl(req, query)
Expand Down

0 comments on commit 40d64ea

Please sign in to comment.