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

improve react-loadable-plugin #24281

Merged
merged 7 commits into from
Apr 21, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions packages/next/build/babel/plugins/react-loadable-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ import {
types as BabelTypes,
} from 'next/dist/compiled/babel/core'

import { relative as relativePath } from 'path'

export default function ({
types: t,
}: {
types: typeof BabelTypes
}): PluginObj {
return {
visitor: {
ImportDeclaration(path: NodePath<BabelTypes.ImportDeclaration>) {
ImportDeclaration(
path: NodePath<BabelTypes.ImportDeclaration>,
state: any
) {
let source = path.node.source.value
if (source !== 'next/dynamic') return

Expand Down Expand Up @@ -133,14 +138,27 @@ export default function ({
if (!loader || Array.isArray(loader)) {
return
}
const dynamicImports: BabelTypes.StringLiteral[] = []
const dynamicImports: BabelTypes.Expression[] = []
const dynamicKeys: BabelTypes.Expression[] = []

loader.traverse({
Import(importPath) {
const importArguments = importPath.parentPath.get('arguments')
if (!Array.isArray(importArguments)) return
const node: any = importArguments[0].node
dynamicImports.push(node)
dynamicKeys.push(
t.binaryExpression(
'+',
t.stringLiteral(
relativePath(
state.file.opts.caller.pagesDir,
state.file.opts.filename
) + ' -> '
),
node
)
)
},
})

Expand Down Expand Up @@ -169,7 +187,7 @@ export default function ({
),
t.objectProperty(
t.identifier('modules'),
t.arrayExpression(dynamicImports)
t.arrayExpression(dynamicKeys)
),
])
)
Expand Down
1 change: 1 addition & 0 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,7 @@ export default async function getBaseWebpackConfig(
!isServer &&
new ReactLoadablePlugin({
filename: REACT_LOADABLE_MANIFEST,
pagesDir,
}),
!isServer && new DropClientPage(),
// Moment.js is an extremely popular library that bundles large locale files
Expand Down
1 change: 1 addition & 0 deletions packages/next/build/webpack/loaders/next-babel-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const customBabelLoader = babelLoader((babel) => {
options.caller.isServer = isServer
options.caller.isDev = development
options.caller.hasJsxRuntime = hasJsxRuntime
options.caller.pagesDir = pagesDir

const emitWarning = this.emitWarning.bind(this)
Object.defineProperty(options.caller, 'onWarning', {
Expand Down
163 changes: 114 additions & 49 deletions packages/next/build/webpack/plugins/react-loadable-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,69 +27,132 @@ import {
sources,
} from 'next/dist/compiled/webpack/webpack'

function getModulesIterable(compilation: any, chunk: any) {
import path from 'path'

function getModuleId(compilation: any, module: any): string | number {
if (isWebpack5) {
return compilation.chunkGraph.getChunkModulesIterable(chunk)
return compilation.chunkGraph.getModuleId(module)
}

return chunk.modulesIterable
return module.id
}

function getModuleId(compilation: any, module: any) {
function getModuleFromDependency(
compilation: any,
dep: any
): webpack.Module & { resource?: string } {
if (isWebpack5) {
return compilation.chunkGraph.getModuleId(module)
return compilation.moduleGraph.getModule(dep)
}

return module.id
return dep.module
}

function buildManifest(
_compiler: webpack.Compiler,
compilation: webpack.compilation.Compilation
) {
let manifest: { [k: string]: any[] } = {}
function getOriginModuleFromDependency(
compilation: any,
dep: any
): webpack.Module & { resource?: string } {
if (isWebpack5) {
return compilation.moduleGraph.getParentModule(dep)
}

compilation.chunkGroups.forEach((chunkGroup) => {
if (chunkGroup.isInitial()) {
return
}
return dep.originModule
}

chunkGroup.origins.forEach((chunkGroupOrigin: any) => {
const { request } = chunkGroupOrigin

chunkGroup.chunks.forEach((chunk: any) => {
chunk.files.forEach((file: string) => {
if (
!(
(file.endsWith('.js') || file.endsWith('.css')) &&
file.match(/^static\/(chunks|css)\//)
)
) {
return
}
function getChunkGroupFromBlock(
compilation: any,
block: any
): webpack.compilation.ChunkGroup {
if (isWebpack5) {
return compilation.chunkGraph.getBlockChunkGroup(block)
}

for (const module of getModulesIterable(compilation, chunk)) {
let id = getModuleId(compilation, module)
return block.chunkGroup
}

if (!manifest[request]) {
manifest[request] = []
}
function buildManifest(
_compiler: webpack.Compiler,
compilation: webpack.compilation.Compilation,
pagesDir: string
) {
let manifest: { [k: string]: { id: string | number; files: string[] } } = {}

// This is allowed:
// import("./module"); <- ImportDependency

// We don't support that:
// import(/* webpackMode: "eager" */ "./module") <- ImportEagerDependency
// import(`./module/${param}`) <- ImportContextDependency

// Find all dependencies blocks which contains a `import()` dependency
const handleBlock = (block: any) => {
block.blocks.forEach(handleBlock)
const chunkGroup = getChunkGroupFromBlock(compilation, block)
for (const dependency of block.dependencies) {
if (dependency.type.startsWith('import()')) {
// get the referenced module
const module = getModuleFromDependency(compilation, dependency)
if (!module) return

// get the module containing the import()
const originModule = getOriginModuleFromDependency(
compilation,
dependency
)
const originRequest: string | undefined = originModule?.resource
if (!originRequest) return

// We construct a "unique" key from origin module and request
// It's not perfect unique, but that will be fine for us.
// We also need to construct the same in the babel plugin.
const key = `${path.relative(pagesDir, originRequest)} -> ${
dependency.request
}`

// Capture all files that need to be loaded.
const files = new Set<string>()

if (manifest[key]) {
// In the "rare" case where multiple chunk groups
// are created for the same `import()` or multiple
// import()s reference the same module, we merge
// the files to make sure to not miss files
// This may cause overfetching in edge cases.
for (const file of manifest[key].files) {
files.add(file)
}
}

// There might not be a chunk group when all modules
// are already loaded. In this case we only need need
// the module id and no files
if (chunkGroup) {
for (const chunk of (chunkGroup as any)
.chunks as webpack.compilation.Chunk[]) {
chunk.files.forEach((file: string) => {
if (
(file.endsWith('.js') || file.endsWith('.css')) &&
file.match(/^static\/(chunks|css)\//)
) {
files.add(file)
}
})
}
}

// Avoid duplicate files
if (
manifest[request].some(
(item) => item.id === id && item.file === file
)
) {
continue
}
// usually we have to add the parent chunk groups too
// but we assume that all parents are also imported by
// next/dynamic so they are loaded by the same technique

manifest[request].push({ id, file })
}
})
})
})
})
// add the id and files to the manifest
const id = getModuleId(compilation, module)
manifest[key] = { id, files: Array.from(files) }
}
}
}
for (const module of compilation.modules) {
module.blocks.forEach(handleBlock)
}

manifest = Object.keys(manifest)
.sort()
Expand All @@ -101,13 +164,15 @@ function buildManifest(

export class ReactLoadablePlugin {
private filename: string
private pagesDir: string

constructor(opts: { filename: string }) {
constructor(opts: { filename: string; pagesDir: string }) {
this.filename = opts.filename
this.pagesDir = opts.pagesDir
}

createAssets(compiler: any, compilation: any, assets: any) {
const manifest = buildManifest(compiler, compilation)
const manifest = buildManifest(compiler, compilation, this.pagesDir)
// @ts-ignore: TODO: remove when webpack 5 is stable
assets[this.filename] = new sources.RawSource(
JSON.stringify(manifest, null, 2)
Expand Down
2 changes: 1 addition & 1 deletion packages/next/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ declare global {
__NEXT_HYDRATED_CB?: () => void

/* prod */
__NEXT_PRELOADREADY?: (ids?: string[]) => void
__NEXT_PRELOADREADY?: (ids?: (string | number)[]) => void
__NEXT_DATA__: NEXT_DATA
__NEXT_P: any[]
}
Expand Down
5 changes: 2 additions & 3 deletions packages/next/next-server/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ParsedUrlQuery } from 'querystring'
import { ComponentType } from 'react'
import { UrlObject } from 'url'
import { formatUrl } from './router/utils/format-url'
import { ManifestItem } from '../server/load-components'
import { NextRouter } from './router/router'
import { Env } from '@next/env'
import { BuildManifest } from '../server/get-page-files'
Expand Down Expand Up @@ -93,7 +92,7 @@ export type NEXT_DATA = {
nextExport?: boolean
autoExport?: boolean
isFallback?: boolean
dynamicIds?: string[]
dynamicIds?: (string | number)[]
err?: Error & { statusCode?: number }
gsp?: boolean
gssp?: boolean
Expand Down Expand Up @@ -185,7 +184,7 @@ export type DocumentProps = DocumentInitialProps & {
inAmpMode: boolean
hybridAmp: boolean
isDevelopment: boolean
dynamicImports: ManifestItem[]
dynamicImports: string[]
assetPrefix?: string
canonicalBase: string
headTags: any[]
Expand Down
5 changes: 2 additions & 3 deletions packages/next/next-server/server/load-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ export function interopDefault(mod: any) {

export type ManifestItem = {
id: number | string
name: string
file: string
files: string[]
}

type ReactLoadableManifest = { [moduleId: string]: ManifestItem[] }
type ReactLoadableManifest = { [moduleId: string]: ManifestItem }

export type LoadComponentsReturnType = {
Component: React.ComponentType
Expand Down
21 changes: 10 additions & 11 deletions packages/next/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ function renderDocument(
ampPath: string
inAmpMode: boolean
hybridAmp: boolean
dynamicImportsIds: string[]
dynamicImports: ManifestItem[]
dynamicImportsIds: (string | number)[]
dynamicImports: string[]
headTags: any
isFallback?: boolean
gsp?: boolean
Expand Down Expand Up @@ -1023,21 +1023,20 @@ export async function renderToHTML(
throw new Error(message)
}

const dynamicImportIdsSet = new Set<string>()
const dynamicImports: ManifestItem[] = []
const dynamicImportsIds = new Set<string | number>()
const dynamicImports = new Set<string>()

for (const mod of reactLoadableModules) {
const manifestItem: ManifestItem[] = reactLoadableManifest[mod]
const manifestItem: ManifestItem = reactLoadableManifest[mod]

if (manifestItem) {
manifestItem.forEach((item) => {
dynamicImports.push(item)
dynamicImportIdsSet.add(item.id as string)
dynamicImportsIds.add(manifestItem.id)
manifestItem.files.forEach((item) => {
dynamicImports.add(item)
})
}
}

const dynamicImportsIds = [...dynamicImportIdsSet]
const hybridAmp = ampState.hybrid

const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}
Expand Down Expand Up @@ -1069,8 +1068,8 @@ export async function renderToHTML(
query,
inAmpMode,
hybridAmp,
dynamicImportsIds,
dynamicImports,
dynamicImportsIds: Array.from(dynamicImportsIds),
dynamicImports: Array.from(dynamicImports),
gsp: !!getStaticProps ? true : undefined,
gssp: !!getServerSideProps ? true : undefined,
gip: hasPageGetInitialProps ? true : undefined,
Expand Down
Loading