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

Optimize bundle size for appDir #42252

Merged
merged 4 commits into from Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
}