Skip to content

Commit

Permalink
Reduce the client bundle size of App Router (vercel#51806)
Browse files Browse the repository at this point in the history
After migrating a Next.js app from Pages Router to App Router and using as many RSC as possible, I notice that the client js bundle size actually increases by 5%. It turns out that Next.js has introduced a lot of code to the client bundle.

<img width="1354" alt="image" src="https://github.com/vercel/next.js/assets/40715044/c7216fee-818b-4593-917e-bf0d2a00967a">

The PR is an attempt to reduce the client bundle size.
  • Loading branch information
SukkaW committed Jun 28, 2023
1 parent e33b87d commit e7a6925
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 134 deletions.
5 changes: 2 additions & 3 deletions packages/next/src/client/app-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@ function loadScriptsInSequence(
})
})
}, Promise.resolve())
.then(() => {
hydrate()
})
.catch((err: Error) => {
console.error(err)
// Still try to hydrate even if there's an error.
})
.then(() => {
hydrate()
})
}
Expand Down
31 changes: 17 additions & 14 deletions packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { GlobalLayoutRouterContext } from '../shared/lib/app-router-context'
import onRecoverableError from './on-recoverable-error'
import { callServer } from './app-call-server'
import { isNextRouterError } from './components/is-next-router-error'
import { linkGc } from './app-link-gc'

// Since React doesn't call onerror for errors caught in error boundaries.
const origConsoleError = window.console.error
Expand Down Expand Up @@ -215,11 +214,12 @@ const StrictModeIfEnabled = process.env.__NEXT_STRICT_MODE_APP
: React.Fragment

function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
React.useEffect(() => {
if (process.env.__NEXT_ANALYTICS_ID) {
if (process.env.__NEXT_ANALYTICS_ID) {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
require('./performance-relayer-app')()
}
}, [])
}, [])
}

if (process.env.__NEXT_TEST_MODE) {
// eslint-disable-next-line react-hooks/rules-of-hooks
Expand All @@ -236,8 +236,7 @@ function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
}

function RSCComponent(props: any): JSX.Element {
const cacheKey = getCacheKey()
return <ServerRoot {...props} cacheKey={cacheKey} />
return <ServerRoot {...props} cacheKey={getCacheKey()} />
}

export function hydrate() {
Expand Down Expand Up @@ -314,14 +313,18 @@ export function hydrate() {
}
}

const reactRoot = isError
? (ReactDOMClient as any).createRoot(appElement, options)
: (React as any).startTransition(() =>
(ReactDOMClient as any).hydrateRoot(appElement, reactEl, options)
)
if (isError) {
reactRoot.render(reactEl)
ReactDOMClient.createRoot(appElement as any, options).render(reactEl)
} else {
React.startTransition(() =>
(ReactDOMClient as any).hydrateRoot(appElement, reactEl, options)
)
}

linkGc()
// TODO-APP: Remove this logic when Float has GC built-in in development.
if (process.env.NODE_ENV !== 'production') {
const { linkGc } =
require('./app-link-gc') as typeof import('./app-link-gc')
linkGc()
}
}
8 changes: 4 additions & 4 deletions packages/next/src/client/components/app-router-announcer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ function getAnnouncerNode() {
const container = document.createElement(ANNOUNCER_TYPE)
container.style.cssText = 'position:absolute'
const announcer = document.createElement('div')
announcer.setAttribute('aria-live', 'assertive')
announcer.setAttribute('id', ANNOUNCER_ID)
announcer.setAttribute('role', 'alert')
announcer.ariaLive = 'assertive'
announcer.id = ANNOUNCER_ID
announcer.role = 'alert'
announcer.style.cssText =
'position:absolute;border:0;height:1px;margin:-1px;padding:0;width:1px;clip:rect(0 0 0 0);overflow:hidden;white-space:nowrap;word-wrap:normal'

Expand Down Expand Up @@ -57,7 +57,7 @@ export function AppRouterAnnouncer({ tree }: { tree: FlightRouterState }) {

// Only announce the title change, but not for the first load because screen
// readers do that automatically.
if (typeof previousTitle.current !== 'undefined') {
if (previousTitle.current !== undefined) {
setRouteAnnouncement(currentTitle)
}
previousTitle.current = currentTitle
Expand Down
94 changes: 43 additions & 51 deletions packages/next/src/client/components/app-router.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
'use client'

import type { ReactNode } from 'react'
import React, { use, useEffect, useMemo, useCallback } from 'react'
import React, {
use,
useEffect,
useMemo,
useCallback,
startTransition,
useInsertionEffect,
} from 'react'
import {
AppRouterContext,
LayoutRouterContext,
Expand Down Expand Up @@ -109,8 +116,7 @@ function isExternalURL(url: URL) {
}

function HistoryUpdater({ tree, pushRef, canonicalUrl, sync }: any) {
// @ts-ignore TODO-APP: useInsertionEffect is available
React.useInsertionEffect(() => {
useInsertionEffect(() => {
// Identifier is shortened intentionally.
// __NA is used to identify if the history entry can be handled by the app-router.
// __N is used to identify if the history entry can be handled by the old router.
Expand All @@ -133,6 +139,13 @@ function HistoryUpdater({ tree, pushRef, canonicalUrl, sync }: any) {
return null
}

const createEmptyCacheNode = () => ({
status: CacheStates.LAZY_INITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
})

/**
* The global router that wraps the application components.
*/
Expand Down Expand Up @@ -203,18 +216,13 @@ function Router({
flightData: FlightData,
overrideCanonicalUrl: URL | undefined
) => {
React.startTransition(() => {
startTransition(() => {
dispatch({
type: ACTION_SERVER_PATCH,
flightData,
previousTree,
overrideCanonicalUrl,
cache: {
status: CacheStates.LAZY_INITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
},
cache: createEmptyCacheNode(),
mutable: {},
})
})
Expand All @@ -237,12 +245,7 @@ function Router({
locationSearch: location.search,
forceOptimisticNavigation,
navigateType,
cache: {
status: CacheStates.LAZY_INITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
},
cache: createEmptyCacheNode(),
mutable: {},
})
},
Expand All @@ -251,7 +254,7 @@ function Router({

const serverActionDispatcher: ServerActionDispatcher = useCallback(
(actionPayload) => {
React.startTransition(() => {
startTransition(() => {
dispatch({
...actionPayload,
type: ACTION_SERVER_ACTION,
Expand Down Expand Up @@ -283,8 +286,7 @@ function Router({
if (isExternalURL(url)) {
return
}
// @ts-ignore startTransition exists
React.startTransition(() => {
startTransition(() => {
dispatch({
type: ACTION_PREFETCH,
url,
Expand All @@ -293,28 +295,20 @@ function Router({
})
},
replace: (href, options = {}) => {
// @ts-ignore startTransition exists
React.startTransition(() => {
startTransition(() => {
navigate(href, 'replace', Boolean(options.forceOptimisticNavigation))
})
},
push: (href, options = {}) => {
// @ts-ignore startTransition exists
React.startTransition(() => {
startTransition(() => {
navigate(href, 'push', Boolean(options.forceOptimisticNavigation))
})
},
refresh: () => {
// @ts-ignore startTransition exists
React.startTransition(() => {
startTransition(() => {
dispatch({
type: ACTION_REFRESH,
cache: {
status: CacheStates.LAZY_INITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
},
cache: createEmptyCacheNode(),
mutable: {},
origin: window.location.origin,
})
Expand All @@ -327,16 +321,10 @@ function Router({
'fastRefresh can only be used in development mode. Please use refresh instead.'
)
} else {
// @ts-ignore startTransition exists
React.startTransition(() => {
startTransition(() => {
dispatch({
type: ACTION_FAST_REFRESH,
cache: {
status: CacheStates.LAZY_INITIALIZED,
data: null,
subTreeData: null,
parallelRoutes: new Map(),
},
cache: createEmptyCacheNode(),
mutable: {},
origin: window.location.origin,
})
Expand All @@ -355,17 +343,21 @@ function Router({
}
}, [appRouter])

useEffect(() => {
// Add `window.nd` for debugging purposes.
// This is not meant for use in applications as concurrent rendering will affect the cache/tree/router.
// @ts-ignore this is for debugging
window.nd = {
router: appRouter,
cache,
prefetchCache,
tree,
}
}, [appRouter, cache, prefetchCache, tree])
if (process.env.NODE_ENV !== 'production') {
// This hook is in a conditional but that is ok because `process.env.NODE_ENV` never changes
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
// Add `window.nd` for debugging purposes.
// This is not meant for use in applications as concurrent rendering will affect the cache/tree/router.
// @ts-ignore this is for debugging
window.nd = {
router: appRouter,
cache,
prefetchCache,
tree,
}
}, [appRouter, cache, prefetchCache, tree])
}

// When mpaNavigation flag is set do a hard navigation to the new url.
// Infinitely suspend because we don't actually want to rerender any child
Expand Down Expand Up @@ -411,7 +403,7 @@ function Router({
// @ts-ignore useTransition exists
// TODO-APP: Ideally the back button should not use startTransition as it should apply the updates synchronously
// Without startTransition works if the cache is there for this path
React.startTransition(() => {
startTransition(() => {
dispatch({
type: ACTION_RESTORE,
url: new URL(window.location.href),
Expand Down
20 changes: 8 additions & 12 deletions packages/next/src/client/components/async-local-storage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { AsyncLocalStorage } from 'async_hooks'

const sharedAsyncLocalStorageNotAvailableError = new Error(
'Invariant: AsyncLocalStorage accessed in runtime where it is not available'
)

class FakeAsyncLocalStorage<Store extends {}>
implements AsyncLocalStorage<Store>
{
disable(): void {
throw new Error(
'Invariant: AsyncLocalStorage accessed in runtime where it is not available'
)
throw sharedAsyncLocalStorageNotAvailableError
}

getStore(): Store | undefined {
Expand All @@ -15,21 +17,15 @@ class FakeAsyncLocalStorage<Store extends {}>
}

run<R>(): R {
throw new Error(
'Invariant: AsyncLocalStorage accessed in runtime where it is not available'
)
throw sharedAsyncLocalStorageNotAvailableError
}

exit<R>(): R {
throw new Error(
'Invariant: AsyncLocalStorage accessed in runtime where it is not available'
)
throw sharedAsyncLocalStorageNotAvailableError
}

enterWith(): void {
throw new Error(
'Invariant: AsyncLocalStorage accessed in runtime where it is not available'
)
throw sharedAsyncLocalStorageNotAvailableError
}
}

Expand Down
Loading

0 comments on commit e7a6925

Please sign in to comment.