Simplify Flight manifest plugin (#51589)

Instead of traversing the entire client module graph twice (!), this PR
changes it to only traverse the client **entry** modules only. Because
of the way we create client entries, all client modules' _boundaries_
can be retrieved via all outgoing connections of all chunks' entry
modules, filtered by `next-flight-client-entry-loader`.

This brings down the time complexity from `2 * num_client_modules` to
`num_client_entry_modules`.

Closes #51240.

Additional notes from @timneutkens 

- Removed `module.unsafeCache` as it seemed to be leaking modules across
multiple compilations, this should help with memory usage
- Changed entry-loader to have consistent module import map (sort it) to
ensure cache key is consistent

---------

Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
This commit is contained in:
Shu Ding 2023-06-26 18:11:08 +02:00 committed by GitHub
parent e140e90e05
commit 4b1a0165d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 66 deletions

View file

@ -2557,12 +2557,6 @@ export default async function getBaseWebpackConfig(
webpack5Config.output.enabledLibraryTypes = ['assign']
}
if (dev) {
// @ts-ignore unsafeCache exists
webpack5Config.module.unsafeCache = (module) =>
!/[\\/]pages[\\/][^\\/]+(?:$|\?|#)/.test(module.resource)
}
// This enables managedPaths for all node_modules
// and also for the unplugged folder when using yarn pnp
// It also add the yarn cache to the immutable paths

View file

@ -31,16 +31,9 @@ export default async function transformSource(this: any): Promise<string> {
.join(';\n')
const buildInfo = getModuleBuildInfo(this._module)
const resolve = this.getResolve()
// Resolve to absolute resource url for flight manifest to collect and use to determine client components
const resolvedRequests = await Promise.all(
requests.map(async (r) => await resolve(this.rootContext, r))
)
buildInfo.rsc = {
type: RSC_MODULE_TYPES.client,
requests: resolvedRequests,
}
return code

View file

@ -675,13 +675,13 @@ export class ClientReferenceEntryPlugin {
// replace them.
const clientLoader = `next-flight-client-entry-loader?${stringify({
modules: this.isEdgeServer
? clientImports.map((importPath) =>
? loaderOptions.modules.map((importPath) =>
importPath.replace(
/[\\/]next[\\/]dist[\\/]esm[\\/]/,
'/next/dist/'.replace(/\//g, path.sep)
)
)
: clientImports,
: loaderOptions.modules,
server: false,
})}!`

View file

@ -12,10 +12,9 @@ import {
SYSTEM_ENTRYPOINTS,
} from '../../../shared/lib/constants'
import { relative } from 'path'
import { isClientComponentEntryModule, isCSSMod } from '../loaders/utils'
import { isCSSMod } from '../loaders/utils'
import { getProxiedPluginState } from '../../build-context'
import { traverseModules } from '../utils'
import { nonNullable } from '../../../lib/non-nullable'
import { WEBPACK_LAYERS } from '../../../lib/constants'
@ -131,20 +130,6 @@ export class ClientReferenceManifestPlugin {
entryCSSFiles: {},
}
const clientRequestsSet = new Set()
// Collect client requests
function collectClientRequest(mod: webpack.NormalModule) {
if (mod.resource === '' && mod.buildInfo?.rsc) {
const { requests = [] } = mod.buildInfo.rsc
requests.forEach((r: string) => {
clientRequestsSet.add(r)
})
}
}
traverseModules(compilation, (mod) => collectClientRequest(mod))
compilation.chunkGroups.forEach((chunkGroup) => {
function getAppPathRequiredChunks() {
return chunkGroup.chunks
@ -189,12 +174,8 @@ export class ClientReferenceManifestPlugin {
}
const recordModule = (id: ModuleId, mod: webpack.NormalModule) => {
const isCSSModule = isCSSMod(mod)
// Skip all modules from the pages folder. CSS modules are a special case
// as they are generated by mini-css-extract-plugin and these modules
// don't have layer information attached.
if (!isCSSModule && mod.layer !== WEBPACK_LAYERS.appClient) {
// Skip all modules from the pages folder.
if (mod.layer !== WEBPACK_LAYERS.appClient) {
return
}
@ -208,13 +189,6 @@ export class ClientReferenceManifestPlugin {
return
}
if (isCSSModule) {
if (chunkEntryName) {
manifest.entryCSSFiles[chunkEntryName].modules.push(resource)
}
return
}
const moduleReferences = manifest.clientModules
const moduleIdMapping = manifest.ssrModuleMapping
const edgeModuleIdMapping = manifest.edgeSSRModuleMapping
@ -230,15 +204,6 @@ export class ClientReferenceManifestPlugin {
if (!ssrNamedModuleId.startsWith('.'))
ssrNamedModuleId = `./${ssrNamedModuleId.replace(/\\/g, '/')}`
// Only apply following logic to client module requests from client entry,
// or if the module is marked as client module.
if (
!clientRequestsSet.has(resource) &&
!isClientComponentEntryModule(mod)
) {
return
}
const isAsyncModule = this.ASYNC_CLIENT_MODULES.has(mod.resource)
// The client compiler will always use the CJS Next.js build, so here we
@ -305,24 +270,73 @@ export class ClientReferenceManifestPlugin {
manifest.edgeSSRModuleMapping = edgeModuleIdMapping
}
// Only apply following logic to client module requests from client entry,
// or if the module is marked as client module. That's because other
// client modules don't need to be in the manifest at all as they're
// never be referenced by the server/client boundary.
// This saves a lot of bytes in the manifest.
chunkGroup.chunks.forEach((chunk: webpack.Chunk) => {
const entryMods =
compilation.chunkGraph.getChunkEntryModulesIterable(chunk)
for (const mod of entryMods) {
if (mod.layer !== WEBPACK_LAYERS.appClient) continue
const request = (mod as webpack.NormalModule).request
if (
!request ||
!request.includes('next-flight-client-entry-loader.js?')
) {
continue
}
const connections =
compilation.moduleGraph.getOutgoingConnections(mod)
for (const connection of connections) {
const dependency = connection.dependency
if (!dependency) continue
const clientEntryMod = compilation.moduleGraph.getResolvedModule(
dependency
) as webpack.NormalModule
const modId = compilation.chunkGraph.getModuleId(clientEntryMod) as
| string
| number
| null
if (modId !== null) {
recordModule(modId, clientEntryMod)
} else {
// If this is a concatenation, register each child to the parent ID.
if (
connection.module?.constructor.name === 'ConcatenatedModule'
) {
const concatenatedMod = connection.module
const concatenatedModId =
compilation.chunkGraph.getModuleId(concatenatedMod)
recordModule(concatenatedModId, clientEntryMod)
}
}
}
}
// Track CSS modules. This is necessary for next/font preloading.
// TODO: Unfortunately we have to keep this iteration for now but we
// will optimize it altogether in the future.
const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
chunk
// TODO: Update type so that it doesn't have to be cast.
) as Iterable<webpack.NormalModule>
for (const mod of chunkModules) {
const modId: string = compilation.chunkGraph.getModuleId(mod) + ''
recordModule(modId, mod)
// If this is a concatenation, register each child to the parent ID.
// TODO: remove any
const anyModule = mod as any
if (anyModule.modules) {
anyModule.modules.forEach((concatenatedMod: any) => {
recordModule(modId, concatenatedMod)
})
if (isCSSMod(mod)) {
if (chunkEntryName) {
const resource =
mod.type === 'css/mini-extract'
? // @ts-expect-error TODO: use `identifier()` instead.
mod._identifier.slice(mod._identifier.lastIndexOf('!') + 1)
: mod.resource
manifest.entryCSSFiles[chunkEntryName].modules.push(resource)
}
}
}
})