diff --git a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts index 16d60c3a0b501..afccbb5e11294 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -14,6 +14,7 @@ import type { import { APP_DIR_ALIAS } from '../../../lib/constants' import { COMPILER_NAMES, + EDGE_RUNTIME_WEBPACK, FLIGHT_SERVER_CSS_MANIFEST, } from '../../../shared/lib/constants' import { FlightCSSManifest, traverseModules } from './flight-manifest-plugin' @@ -84,35 +85,51 @@ export class FlightClientEntryPlugin { ReturnType > = [] - // For each SC server compilation entry, we need to create its corresponding - // client component entry. - for (const [name, entry] of compilation.entries.entries()) { - // Check if the page entry is a server component or not. - const entryDependency: webpack.NormalModule | undefined = - entry.dependencies?.[0] - // Ensure only next-app-loader entries are handled. - if (!entryDependency || !entryDependency.request) continue - - const request = entryDependency.request + // Loop over all the entry modules. + function forEachEntryModule( + callback: ({ + name, + entryModule, + }: { + name: string + entryModule: any + }) => void + ) { + for (const [name, entry] of compilation.entries.entries()) { + // Check if the page entry is a server component or not. + const entryDependency: webpack.NormalModule | undefined = + entry.dependencies?.[0] + // Ensure only next-app-loader entries are handled. + if (!entryDependency || !entryDependency.request) continue + + const request = entryDependency.request + + if ( + !request.startsWith('next-edge-ssr-loader?') && + !request.startsWith('next-app-loader?') + ) + continue - if ( - !request.startsWith('next-edge-ssr-loader?') && - !request.startsWith('next-app-loader?') - ) - continue + let entryModule: webpack.NormalModule = + compilation.moduleGraph.getResolvedModule(entryDependency) - let entryModule: webpack.NormalModule = - compilation.moduleGraph.getResolvedModule(entryDependency) + if (request.startsWith('next-edge-ssr-loader?')) { + entryModule.dependencies.forEach((dependency) => { + const modRequest: string | undefined = (dependency as any).request + if (modRequest?.includes('next-app-loader')) { + entryModule = + compilation.moduleGraph.getResolvedModule(dependency) + } + }) + } - if (request.startsWith('next-edge-ssr-loader?')) { - entryModule.dependencies.forEach((dependency) => { - const modRequest: string | undefined = (dependency as any).request - if (modRequest?.includes('next-app-loader')) { - entryModule = compilation.moduleGraph.getResolvedModule(dependency) - } - }) + callback({ name, entryModule }) } + } + // For each SC server compilation entry, we need to create its corresponding + // client component entry. + forEachEntryModule(({ name, entryModule }) => { const internalClientComponentEntryImports = new Set< ClientComponentImports[0] >() @@ -123,15 +140,13 @@ export class FlightClientEntryPlugin { const layoutOrPageDependency = connection.dependency const layoutOrPageRequest = connection.dependency.request - const [clientComponentImports, cssImports] = + const [clientComponentImports] = this.collectClientComponentsAndCSSForDependency({ layoutOrPageRequest, compilation, dependency: layoutOrPageDependency, }) - Object.assign(flightCSSManifest, cssImports) - const isAbsoluteRequest = layoutOrPageRequest[0] === '/' // Next.js internals are put into a separate entry. @@ -170,7 +185,28 @@ export class FlightClientEntryPlugin { bundlePath: 'app-internals', }) ) - } + }) + + // After optimizing all the modules, we collect the CSS that are still used. + compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => { + forEachEntryModule(({ entryModule }) => { + for (const connection of compilation.moduleGraph.getOutgoingConnections( + entryModule + )) { + const layoutOrPageDependency = connection.dependency + const layoutOrPageRequest = connection.dependency.request + + const [, cssImports] = + this.collectClientComponentsAndCSSForDependency({ + layoutOrPageRequest, + compilation, + dependency: layoutOrPageDependency, + }) + + Object.assign(flightCSSManifest, cssImports) + } + }) + }) compilation.hooks.processAssets.tap( { @@ -261,6 +297,21 @@ export class FlightClientEntryPlugin { const isClientComponent = isClientComponentModule(mod) if (isCSS) { + const sideEffectFree = + mod.factoryMeta && (mod.factoryMeta as any).sideEffectFree + + if (sideEffectFree) { + const unused = !compilation.moduleGraph + .getExportsInfo(mod) + .isModuleUsed( + this.isEdgeServer ? EDGE_RUNTIME_WEBPACK : 'webpack-runtime' + ) + + if (unused) { + return + } + } + serverCSSImports[layoutOrPageRequest] = serverCSSImports[layoutOrPageRequest] || [] serverCSSImports[layoutOrPageRequest].push(modRequest) diff --git a/test/e2e/app-dir/app/app/css/css-page/page.js b/test/e2e/app-dir/app/app/css/css-page/page.js index 2fbdf86dedd54..10799b9671766 100644 --- a/test/e2e/app-dir/app/app/css/css-page/page.js +++ b/test/e2e/app-dir/app/app/css/css-page/page.js @@ -1,4 +1,5 @@ import './style.css' + import styles from './style.module.css' export default function Page() { diff --git a/test/e2e/app-dir/app/app/css/css-page/unused/page.js b/test/e2e/app-dir/app/app/css/css-page/unused/page.js new file mode 100644 index 0000000000000..b3fa35d7cf2b6 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/css-page/unused/page.js @@ -0,0 +1,12 @@ +import { styles } from './styles' + +export default function Page() { + return ( + <> +

Page

+
+ CSSM +
+ + ) +} diff --git a/test/e2e/app-dir/app/app/css/css-page/unused/styles.js b/test/e2e/app-dir/app/app/css/css-page/unused/styles.js new file mode 100644 index 0000000000000..d191a9a4e1560 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/css-page/unused/styles.js @@ -0,0 +1,4 @@ +import unused from './unused.module.css' +import styles from '../style.module.css' + +export { unused, styles } diff --git a/test/e2e/app-dir/app/app/css/css-page/unused/unused.module.css b/test/e2e/app-dir/app/app/css/css-page/unused/unused.module.css new file mode 100644 index 0000000000000..67ed738ab1f52 --- /dev/null +++ b/test/e2e/app-dir/app/app/css/css-page/unused/unused.module.css @@ -0,0 +1,3 @@ +.this_should_not_be_included { + color: red; +} diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 24470ecca6683..c67d345bb0d35 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1186,6 +1186,17 @@ describe('app dir', () => { ) ).toBe('rgb(0, 0, 255)') }) + + if (!isDev) { + it('should not include unused css modules in the page in prod', async () => { + const browser = await webdriver(next.url, '/css/css-page') + expect( + await browser.eval( + `[...document.styleSheets].some(({ rules }) => [...rules].some(rule => rule.selectorText.includes('this_should_not_be_included')))` + ) + ).toBe(false) + }) + } }) describe('client layouts', () => {