Skip to content

Commit

Permalink
Replace page loader with new route loader (#19006)
Browse files Browse the repository at this point in the history
This pull request completely replaces our old page loader with a brand new route loader.

Our existing comprehensive test suite means I did not need to add a bunch of tests. I did add them where behavior was added or fixed.

Summary of the changes:

- Eagerly evaluates prefetched pages in browser idle time (speeds up transitions)
- Router is **no longer frozen** indefinitely if the Build Manifest never arrives
- Router is **no longer frozen** indefinitely if a page fails to bootstrap
- New `withFuture` utility instead of ad-hoc deduping per resource
- Prefetching is now delayed until browser idle time to not impact TTI
- Browsers without `prefetch` now fall back to eager evaluation instead of using `preload`
- We're now ready to serve non-static assets **with `no-store` without breaking prefetching**
- **Application can now hydrate without fetching CSS assets—this is a huge performance win that was previously blocking hydration**

---

The minor size increase here is unfortunate, but we have to incur it for correctness.

---

Fixes #18389
Fixes #18642
  • Loading branch information
Timer authored Nov 11, 2020
1 parent 9cda047 commit 0d5bf65
Show file tree
Hide file tree
Showing 14 changed files with 545 additions and 426 deletions.
69 changes: 33 additions & 36 deletions packages/next/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ import * as envConfig from '../next-server/lib/runtime-config'
import type { NEXT_DATA } from '../next-server/lib/utils'
import { getURL, loadGetInitialProps, ST } from '../next-server/lib/utils'
import initHeadManager from './head-manager'
import PageLoader, {
INITIAL_CSS_LOAD_ERROR,
looseToArray,
StyleSheetTuple,
} from './page-loader'
import PageLoader, { StyleSheetTuple } from './page-loader'
import measureWebVitals from './performance-relayer'
import { createRouter, makePublicRouterInstance } from './router'

Expand Down Expand Up @@ -53,6 +49,8 @@ window.__NEXT_DATA__ = data

export const version = process.env.__NEXT_VERSION

const looseToArray = <T extends {}>(input: any): T[] => [].slice.call(input)

const {
props: hydrateProps,
err: hydrateErr,
Expand Down Expand Up @@ -133,8 +131,9 @@ if (process.env.__NEXT_I18N_SUPPORT) {

type RegisterFn = (input: [string, () => void]) => void

const pageLoader = new PageLoader(buildId, prefix, page)
const register: RegisterFn = ([r, f]) => pageLoader.registerPage(r, f)
const pageLoader = new PageLoader(buildId, prefix)
const register: RegisterFn = ([r, f]) =>
pageLoader.routeLoader.onEntrypoint(r, f)
if (window.__NEXT_P) {
// Defer page registration for another tick. This will increase the overall
// latency in hydrating the page, but reduce the total blocking time.
Expand All @@ -151,7 +150,6 @@ let lastRenderReject: (() => void) | null
let webpackHMR: any
export let router: Router
let CachedComponent: React.ComponentType
let cachedStyleSheets: StyleSheetTuple[]
let CachedApp: AppComponent, onPerfEntry: (metric: any) => void

class Container extends React.Component<{
Expand Down Expand Up @@ -236,7 +234,13 @@ export default async (opts: { webpackHMR?: any } = {}) => {
if (process.env.NODE_ENV === 'development') {
webpackHMR = opts.webpackHMR
}
const { page: app, mod } = await pageLoader.loadPage('/_app')

const appEntrypoint = await pageLoader.routeLoader.whenEntrypoint('/_app')
if ('error' in appEntrypoint) {
throw appEntrypoint.error
}

const { component: app, exports: mod } = appEntrypoint
CachedApp = app as AppComponent

if (mod && mod.reportWebVitals) {
Expand Down Expand Up @@ -275,10 +279,16 @@ export default async (opts: { webpackHMR?: any } = {}) => {
let initialErr = hydrateErr

try {
;({
page: CachedComponent,
styleSheets: cachedStyleSheets,
} = await pageLoader.loadPage(page))
const pageEntrypoint =
// The dev server fails to serve script assets when there's a hydration
// error, so we need to skip waiting for the entrypoint.
process.env.NODE_ENV === 'development' && hydrateErr
? { error: hydrateErr }
: await pageLoader.routeLoader.whenEntrypoint(page)
if ('error' in pageEntrypoint) {
throw pageEntrypoint.error
}
CachedComponent = pageEntrypoint.component

if (process.env.NODE_ENV !== 'production') {
const { isValidElementType } = require('react-is')
Expand All @@ -289,9 +299,6 @@ export default async (opts: { webpackHMR?: any } = {}) => {
}
}
} catch (error) {
if (INITIAL_CSS_LOAD_ERROR in error) {
throw error
}
// This catches errors like throwing in the top level of a module
initialErr = error
}
Expand Down Expand Up @@ -339,12 +346,10 @@ export default async (opts: { webpackHMR?: any } = {}) => {
pageLoader,
App: CachedApp,
Component: CachedComponent,
initialStyleSheets: cachedStyleSheets,
wrapApp,
err: initialErr,
isFallback: Boolean(isFallback),
subscription: ({ Component, styleSheets, props, err }, App) =>
render({ App, Component, styleSheets, props, err }),
subscription: (info, App) => render(Object.assign({}, info, { App })),
locale,
locales,
defaultLocale,
Expand All @@ -363,10 +368,10 @@ export default async (opts: { webpackHMR?: any } = {}) => {
})
}

const renderCtx = {
const renderCtx: RenderRouteInfo = {
App: CachedApp,
initial: true,
Component: CachedComponent,
styleSheets: cachedStyleSheets,
props: hydrateProps,
err: initialErr,
}
Expand Down Expand Up @@ -471,8 +476,6 @@ export function renderError(renderErrorProps: RenderErrorProps) {
})
}

// If hydrate does not exist, eg in preact.
let isInitialRender = typeof ReactDOM.hydrate === 'function'
let reactRoot: any = null
function renderReactElement(reactEl: JSX.Element, domEl: HTMLElement) {
if (process.env.__NEXT_REACT_MODE !== 'legacy') {
Expand All @@ -491,9 +494,8 @@ function renderReactElement(reactEl: JSX.Element, domEl: HTMLElement) {
}

// The check for `.hydrate` is there to support React alternatives like preact
if (isInitialRender) {
if (typeof ReactDOM.hydrate === 'function') {
ReactDOM.hydrate(reactEl, domEl, markHydrateComplete)
isInitialRender = false
} else {
ReactDOM.render(reactEl, domEl, markRenderComplete)
}
Expand Down Expand Up @@ -591,13 +593,10 @@ const wrapApp = (App: AppComponent) => (
)
}

function doRender({
App,
Component,
props,
err,
styleSheets,
}: RenderRouteInfo): Promise<any> {
function doRender(input: RenderRouteInfo): Promise<any> {
let { App, Component, props, err } = input
let styleSheets: StyleSheetTuple[] | undefined =
'initial' in input ? undefined : input.styleSheets
Component = Component || lastAppProps.Component
props = props || lastAppProps.props

Expand Down Expand Up @@ -634,9 +633,7 @@ function doRender({
// Promise. It should remain synchronous.
function onStart(): boolean {
if (
// We can skip this during hydration. Running it wont cause any harm, but
// we may as well save the CPU cycles.
isInitialRender ||
!styleSheets ||
// We use `style-loader` in development, so we don't need to do anything
// unless we're in production:
process.env.NODE_ENV !== 'production'
Expand Down Expand Up @@ -671,7 +668,7 @@ function doRender({
process.env.NODE_ENV === 'production' &&
// We can skip this during hydration. Running it wont cause any harm, but
// we may as well save the CPU cycles:
!isInitialRender &&
styleSheets &&
// Ensure this render was not canceled
!canceled
) {
Expand Down
Loading

0 comments on commit 0d5bf65

Please sign in to comment.