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:
parent
e140e90e05
commit
4b1a0165d3
4 changed files with 67 additions and 66 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})}!`
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue