Skip to content

Commit

Permalink
Optimize bundle size for appDir (#42252)
Browse files Browse the repository at this point in the history
- Ensure React / other deps are deduped against main-app bundle
- Only require web-vitals when it's needed
- Move warnOnce into separate file as it's not tree shaken
- Add create-next-app with bundle analyzer

<details>
<summary>13.0.0 (react-dom deduping bug)</summary>

<img width="1912" alt="13-0-0" src="https://user-images.githubusercontent.com/6324199/199067942-d2394ffa-fc1c-4606-94f4-b489ef959a9b.png">

</details>

<details>
<summary>Canary (react-dom deduping bug fixed)</summary>

<img width="1912" alt="current-canary-branch" src="https://user-images.githubusercontent.com/6324199/199067999-786b523e-b4f0-4044-8d2e-acaa0386771a.png">

</details>

<details>
<summary>This PR</summary>

<img width="1912" alt="pr-changes" src="https://user-images.githubusercontent.com/6324199/199068060-2fdb6e11-8b94-4683-9b8e-65bb6faab34a.png">

</details>



## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
timneutkens committed Oct 31, 2022
1 parent 848bb3a commit 8e3586d
Show file tree
Hide file tree
Showing 15 changed files with 414 additions and 19 deletions.
5 changes: 4 additions & 1 deletion packages/next/build/index.ts
Expand Up @@ -984,7 +984,10 @@ export default async function build(
value,
]
} else {
clientEntry[key] = value
clientEntry[key] = {
dependOn: [CLIENT_STATIC_FILES_RUNTIME_MAIN_APP],
import: value,
}
}
})

Expand Down
5 changes: 3 additions & 2 deletions packages/next/client/app-index.tsx
Expand Up @@ -5,7 +5,6 @@ import ReactDOMClient from 'react-dom/client'
import React, { use } from 'react'
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack/client'

import measureWebVitals from './performance-relayer'
import { HeadManagerContext } from '../shared/lib/head-manager-context'
import { GlobalLayoutRouterContext } from '../shared/lib/app-router-context'

Expand Down Expand Up @@ -155,7 +154,9 @@ const StrictModeIfEnabled = process.env.__NEXT_STRICT_MODE_APP

function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
React.useEffect(() => {
measureWebVitals()
if (process.env.__NEXT_ANALYTICS_ID) {
require('./performance-relayer-app')()
}
}, [])

if (process.env.__NEXT_TEST_MODE) {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/client/image.tsx
Expand Up @@ -17,7 +17,7 @@ import {
ImageLoaderPropsWithConfig,
} from '../shared/lib/image-config'
import { ImageConfigContext } from '../shared/lib/image-config-context'
import { warnOnce } from '../shared/lib/utils'
import { warnOnce } from '../shared/lib/utils/warn-once'
// @ts-ignore - This is replaced by webpack alias
import defaultLoader from 'next/dist/shared/lib/image-loader'

Expand Down
2 changes: 1 addition & 1 deletion packages/next/client/legacy/image.tsx
Expand Up @@ -17,7 +17,7 @@ import {
} from '../../shared/lib/image-config'
import { useIntersection } from '../use-intersection'
import { ImageConfigContext } from '../../shared/lib/image-config-context'
import { warnOnce } from '../../shared/lib/utils'
import { warnOnce } from '../../shared/lib/utils/warn-once'
import { normalizePathTrailingSlash } from '../normalize-trailing-slash'

function normalizeSrc(src: string): string {
Expand Down
103 changes: 103 additions & 0 deletions packages/next/client/performance-relayer-app.ts
@@ -0,0 +1,103 @@
/* global location */
import type { Metric, ReportCallback } from 'next/dist/compiled/web-vitals'

// copied to prevent pulling in un-necessary utils
const WEB_VITALS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB']

const initialHref = location.href
let isRegistered = false
let userReportHandler: ReportCallback | undefined
type Attribution = typeof WEB_VITALS[number]

function onReport(metric: Metric): void {
if (userReportHandler) {
userReportHandler(metric)
}

// This code is not shipped, executed, or present in the client-side
// JavaScript bundle unless explicitly enabled in your application.
//
// When this feature is enabled, we'll make it very clear by printing a
// message during the build (`next build`).
if (
process.env.NODE_ENV === 'production' &&
// This field is empty unless you explicitly configure it:
process.env.__NEXT_ANALYTICS_ID
) {
const body: Record<string, string> = {
dsn: process.env.__NEXT_ANALYTICS_ID,
id: metric.id,
page: window.__NEXT_DATA__?.page,
href: initialHref,
event_name: metric.name,
value: metric.value.toString(),
speed:
'connection' in navigator &&
(navigator as any)['connection'] &&
'effectiveType' in (navigator as any)['connection']
? ((navigator as any)['connection']['effectiveType'] as string)
: '',
}

const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`:
type: 'application/x-www-form-urlencoded',
})
const vitalsUrl = 'https://vitals.vercel-insights.com/v1/vitals'
// Navigator has to be bound to ensure it does not error in some browsers
// https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
const send = navigator.sendBeacon && navigator.sendBeacon.bind(navigator)

function fallbackSend() {
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
// console.error is used here as when the fetch fails it does not affect functioning of the app
}).catch(console.error)
}

try {
// If send is undefined it'll throw as well. This reduces output code size.
send!(vitalsUrl, blob) || fallbackSend()
} catch (err) {
fallbackSend()
}
}
}

export default (onPerfEntry?: ReportCallback): void => {
if (process.env.__NEXT_ANALYTICS_ID) {
// Update function if it changes:
userReportHandler = onPerfEntry

// Only register listeners once:
if (isRegistered) {
return
}
isRegistered = true

const attributions: Attribution[] | undefined = process.env
.__NEXT_WEB_VITALS_ATTRIBUTION as any

for (const webVital of WEB_VITALS) {
try {
let mod: any

if (process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION) {
if (attributions?.includes(webVital)) {
mod = require('next/dist/compiled/web-vitals-attribution')
}
}
if (!mod) {
mod = require('next/dist/compiled/web-vitals')
}
mod[`on${webVital}`](onReport)
} catch (err) {
// Do nothing if the module fails to load
console.warn(`Failed to track ${webVital} web-vital`, err)
}
}
}
}
2 changes: 1 addition & 1 deletion packages/next/shared/lib/head.tsx
Expand Up @@ -5,7 +5,7 @@ import Effect from './side-effect'
import { AmpStateContext } from './amp-context'
import { HeadManagerContext } from './head-manager-context'
import { isInAmpMode } from './amp-mode'
import { warnOnce } from './utils'
import { warnOnce } from './utils/warn-once'

type WithInAmpMode = {
inAmpMode?: boolean
Expand Down
13 changes: 0 additions & 13 deletions packages/next/shared/lib/utils.ts
Expand Up @@ -400,19 +400,6 @@ export async function loadGetInitialProps<
return props
}

let warnOnce = (_: string) => {}
if (process.env.NODE_ENV !== 'production') {
const warnings = new Set<string>()
warnOnce = (msg: string) => {
if (!warnings.has(msg)) {
console.warn(msg)
}
warnings.add(msg)
}
}

export { warnOnce }

export const SP = typeof performance !== 'undefined'
export const ST =
SP &&
Expand Down
12 changes: 12 additions & 0 deletions packages/next/shared/lib/utils/warn-once.ts
@@ -0,0 +1,12 @@
let warnOnce = (_: string) => {}
if (process.env.NODE_ENV !== 'production') {
const warnings = new Set<string>()
warnOnce = (msg: string) => {
if (!warnings.has(msg)) {
console.warn(msg)
}
warnings.add(msg)
}
}

export { warnOnce }
@@ -0,0 +1,4 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
26 changes: 26 additions & 0 deletions test/e2e/app-dir/create-next-app-template/app/globals.css
@@ -0,0 +1,26 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
color: inherit;
text-decoration: none;
}

* {
box-sizing: border-box;
}

@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
18 changes: 18 additions & 0 deletions test/e2e/app-dir/create-next-app-template/app/layout.tsx
@@ -0,0 +1,18 @@
import './globals.css'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</head>
<body>{children}</body>
</html>
)
}
146 changes: 146 additions & 0 deletions test/e2e/app-dir/create-next-app-template/app/page.module.css
@@ -0,0 +1,146 @@
.container {
padding: 0 2rem;
}

.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}

.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}

.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
font-style: normal;
font-weight: 800;
letter-spacing: -0.025em;
}

.title a {
text-decoration: none;
color: #0070f3;
}

.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}

.title,
.description {
text-align: center;
}

.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}

.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 1200px;
}

.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}

.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}

.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}

.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}

.logo {
height: 1em;
margin-left: 0.5rem;
}

@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

@media (prefers-color-scheme: dark) {
.title {
background: linear-gradient(180deg, #ffffff 0%, #aaaaaa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.title a {
background: linear-gradient(180deg, #0070f3 0%, #0153af 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}

0 comments on commit 8e3586d

Please sign in to comment.