From f0b9474b40312a0c24cf520ffe76db0cdb9094bd Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 21 Oct 2021 20:51:44 +0100 Subject: [PATCH] feat(nuxi): bundle analyzer (#701) Co-authored-by: Pooya Parsa --- packages/kit/src/config/schema/build.ts | 5 +- packages/nitro/package.json | 2 +- packages/nitro/src/context.ts | 5 +- packages/nitro/src/rollup/config.ts | 16 +++-- packages/nuxi/src/commands/analyze.ts | 77 +++++++++++++++++++++++++ packages/nuxi/src/commands/index.ts | 1 + packages/nuxi/src/utils/server.ts | 6 +- packages/vite/package.json | 1 + packages/vite/src/client.ts | 29 ++++++++++ packages/webpack/src/configs/client.ts | 2 +- yarn.lock | 38 +++++++++--- 11 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 packages/nuxi/src/commands/analyze.ts diff --git a/packages/kit/src/config/schema/build.ts b/packages/kit/src/config/schema/build.ts index 862d237add..ce9b4b71bb 100644 --- a/packages/kit/src/config/schema/build.ts +++ b/packages/kit/src/config/schema/build.ts @@ -15,9 +15,7 @@ export default { /** * Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them. * - * This option is normally enabled by the CLI argument `--analyze`. - * - * Set to `true` to enable bundle analysis, or pass [an object with options](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin). + * Set to `true` to enable bundle analysis, or pass an object with options: [for webpack](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin) or [for vite](https://github.com/btd/rollup-plugin-visualizer#options). * * @example * ```js @@ -25,7 +23,6 @@ export default { * analyzerMode: 'static' * } * ``` - * @version 2 */ analyze: false, diff --git a/packages/nitro/package.json b/packages/nitro/package.json index af3997a67a..f1e895ca9e 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -58,8 +58,8 @@ "pathe": "^0.2.0", "pretty-bytes": "^5.6.0", "rollup": "^2.58.0", - "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-visualizer": "^5.5.2", "serve-placeholder": "^1.2.4", "serve-static": "^1.14.1", "std-env": "^2.3.1", diff --git a/packages/nitro/src/context.ts b/packages/nitro/src/context.ts index 64d3d4adc9..edf432670f 100644 --- a/packages/nitro/src/context.ts +++ b/packages/nitro/src/context.ts @@ -4,6 +4,7 @@ import defu from 'defu' import { createHooks, Hookable, NestedHooks } from 'hookable' import type { Preset } from 'unenv' import type { NuxtHooks, NuxtOptions } from '@nuxt/kit' +import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer' import { tryImport, resolvePath, detectTarget, extendPreset } from './utils' import * as PRESETS from './presets' import type { NodeExternalsOptions } from './rollup/plugins/externals' @@ -28,7 +29,7 @@ export interface NitroContext { minify: boolean sourceMap: boolean externals: boolean | NodeExternalsOptions - analyze: boolean + analyze: false | PluginVisualizerOptions entry: string node: boolean preset: string @@ -94,7 +95,7 @@ export function getNitroContext (nuxtOptions: NuxtOptions, input: NitroInput): N minify: undefined, sourceMap: undefined, externals: undefined, - analyze: undefined, + analyze: nuxtOptions.build.analyze as any, entry: undefined, node: undefined, preset: undefined, diff --git a/packages/nitro/src/rollup/config.ts b/packages/nitro/src/rollup/config.ts index 78a8c04cee..d01b83a5d8 100644 --- a/packages/nitro/src/rollup/config.ts +++ b/packages/nitro/src/rollup/config.ts @@ -12,7 +12,7 @@ import replace from '@rollup/plugin-replace' import virtual from '@rollup/plugin-virtual' import wasmPlugin from '@rollup/plugin-wasm' import inject from '@rollup/plugin-inject' -import analyze from 'rollup-plugin-analyzer' +import { visualizer } from 'rollup-plugin-visualizer' import * as unenv from 'unenv' import type { Preset } from 'unenv' @@ -309,11 +309,6 @@ export const getRollupConfig = (nitroContext: NitroContext) => { // https://github.com/rollup/plugins/tree/master/packages/inject rollupConfig.plugins.push(inject(env.inject)) - if (nitroContext.analyze) { - // https://github.com/doesdev/rollup-plugin-analyzer - rollupConfig.plugins.push(analyze()) - } - // https://github.com/TrySound/rollup-plugin-terser // https://github.com/terser/terser#minify-nitroContext if (nitroContext.minify) { @@ -328,5 +323,14 @@ export const getRollupConfig = (nitroContext: NitroContext) => { })) } + if (nitroContext.analyze) { + // https://github.com/btd/rollup-plugin-visualizer + rollupConfig.plugins.push(visualizer({ + ...nitroContext.analyze, + filename: nitroContext.analyze.filename.replace('{name}', 'nitro'), + title: 'Nitro Server bundle stats' + })) + } + return rollupConfig } diff --git a/packages/nuxi/src/commands/analyze.ts b/packages/nuxi/src/commands/analyze.ts new file mode 100644 index 0000000000..98aa4a23ea --- /dev/null +++ b/packages/nuxi/src/commands/analyze.ts @@ -0,0 +1,77 @@ +import { promises as fsp } from 'fs' +import { join, resolve } from 'pathe' +import { createApp, lazyHandle } from 'h3' +import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer' +import { createServer } from '../utils/server' +import { writeTypes } from '../utils/prepare' +import { loadKit } from '../utils/kit' +import { clearDir } from '../utils/fs' +import { defineNuxtCommand } from './index' + +export default defineNuxtCommand({ + meta: { + name: 'analyze', + usage: 'npx nuxi analyze [rootDir]', + description: 'Build nuxt and analyze production bundle (experimental)' + }, + async invoke (args) { + process.env.NODE_ENV = process.env.NODE_ENV || 'production' + + const rootDir = resolve(args._[0] || '.') + const statsDir = join(rootDir, '.nuxt/stats') + + const { loadNuxt, buildNuxt } = await loadKit(rootDir) + + const analyzeOptions: PluginVisualizerOptions = { + template: 'treemap', + projectRoot: rootDir, + filename: join(statsDir, '{name}.html') + } + + const nuxt = await loadNuxt({ + rootDir, + config: { + build: { + // @ts-ignore + analyze: analyzeOptions + } + } + }) + + await clearDir(nuxt.options.buildDir) + await writeTypes(nuxt) + await buildNuxt(nuxt) + + const app = createApp() + const server = createServer(app) + + const serveFile = (filePath: string) => lazyHandle(async () => { + const contents = await fsp.readFile(filePath, 'utf-8') + return (_req, res) => { res.end(contents) } + }) + + console.warn('Do not deploy analyze results! Use `nuxi build` before deployng.') + + console.info('Starting stats server...') + + app.use('/client', serveFile(join(statsDir, 'client.html'))) + app.use('/nitro', serveFile(join(statsDir, 'nitro.html'))) + app.use(() => ` + + +<meta charset="utf-8">Nuxt Bundle Stats (experimental) + +

Nuxt Bundle Stats (experimental)

+ +`) + + await server.listen() + } +}) diff --git a/packages/nuxi/src/commands/index.ts b/packages/nuxi/src/commands/index.ts index 90c58785fc..1051d57dd1 100644 --- a/packages/nuxi/src/commands/index.ts +++ b/packages/nuxi/src/commands/index.ts @@ -5,6 +5,7 @@ const _rDefault = r => r.default || r export const commands = { dev: () => import('./dev').then(_rDefault), build: () => import('./build').then(_rDefault), + analyze: () => import('./analyze').then(_rDefault), generate: () => import('./generate').then(_rDefault), prepare: () => import('./prepare').then(_rDefault), usage: () => import('./usage').then(_rDefault), diff --git a/packages/nuxi/src/utils/server.ts b/packages/nuxi/src/utils/server.ts index 046afe8f2d..43aaf5223e 100644 --- a/packages/nuxi/src/utils/server.ts +++ b/packages/nuxi/src/utils/server.ts @@ -2,10 +2,10 @@ import type { RequestListener } from 'http' import type { ListenOptions } from 'listhen' import { loading } from '@nuxt/design' -export function createServer () { - const listener = createDynamicFunction(createLoadingHandler('Loading...')) +export function createServer (defaultApp?) { + const listener = createDynamicFunction(defaultApp || createLoadingHandler('Loading...')) - async function listen (opts: Partial) { + async function listen (opts?: Partial) { const { listen } = await import('listhen') return listen(listener.call, opts) } diff --git a/packages/vite/package.json b/packages/vite/package.json index 786287280f..b99a6455e5 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -32,6 +32,7 @@ "postcss-import": "^14.0.2", "postcss-import-resolver": "^2.0.0", "postcss-url": "^10.1.3", + "rollup-plugin-visualizer": "^5.5.2", "ufo": "^0.7.9", "vite": "^2.6.10" }, diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index c21c47cb4d..0b920163db 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -5,6 +5,8 @@ import vitePlugin from '@vitejs/plugin-vue' import viteJsxPlugin from '@vitejs/plugin-vue-jsx' import type { Connect } from 'vite' +import { visualizer } from 'rollup-plugin-visualizer' +import { transform } from 'esbuild' import { cacheDirPlugin } from './plugins/cache-dir' import { replace } from './plugins/replace' import { wpfs } from './utils/wpfs' @@ -45,6 +47,33 @@ export async function buildClient (ctx: ViteBuildContext) { } } as ViteOptions) + // Add analyze plugin if needed + if (ctx.nuxt.options.build.analyze) { + clientConfig.plugins.push({ + name: 'nuxt-analyze-minify', + async generateBundle (_opts, outputBundle) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_bundleId, bundle] of Object.entries(outputBundle)) { + if (bundle.type !== 'chunk') { continue } + const originalEntries = Object.entries(bundle.modules) + const minifiedEntries = await Promise.all(originalEntries.map(async ([moduleId, module]) => { + const { code } = await transform(module.code || '', { minify: true }) + return [moduleId, { ...module, code }] + })) + bundle.modules = Object.fromEntries(minifiedEntries) + } + return null + } + }) + clientConfig.plugins.push(visualizer({ + ...ctx.nuxt.options.build.analyze as any, + // @ts-ignore + filename: ctx.nuxt.options.build.analyze.filename.replace('{name}', 'client'), + title: 'Client bundle stats', + gzipSize: true + })) + } + await ctx.nuxt.callHook('vite:extendConfig', clientConfig, { isClient: true, isServer: false }) const viteServer = await vite.createServer(clientConfig) diff --git a/packages/webpack/src/configs/client.ts b/packages/webpack/src/configs/client.ts index 834ac2365c..b1ee9cd113 100644 --- a/packages/webpack/src/configs/client.ts +++ b/packages/webpack/src/configs/client.ts @@ -80,7 +80,7 @@ function clientPlugins (ctx: WebpackConfigContext) { // Webpack Bundle Analyzer // https://github.com/webpack-contrib/webpack-bundle-analyzer - if (!ctx.isDev && options.build.analyze) { + if (!ctx.isDev && ctx.name === 'client' && options.build.analyze) { const statsDir = resolve(options.buildDir, 'stats') // @ts-ignore diff --git a/yarn.lock b/yarn.lock index 205ead75ea..c30845598c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2673,8 +2673,8 @@ __metadata: pathe: ^0.2.0 pretty-bytes: ^5.6.0 rollup: ^2.58.0 - rollup-plugin-analyzer: ^4.0.0 rollup-plugin-terser: ^7.0.2 + rollup-plugin-visualizer: ^5.5.2 serve-placeholder: ^1.2.4 serve-static: ^1.14.1 std-env: ^2.3.1 @@ -2815,6 +2815,7 @@ __metadata: postcss-import: ^14.0.2 postcss-import-resolver: ^2.0.0 postcss-url: ^10.1.3 + rollup-plugin-visualizer: ^5.5.2 ufo: ^0.7.9 unbuild: latest vite: ^2.6.10 @@ -13158,7 +13159,7 @@ fsevents@~2.3.2: languageName: node linkType: hard -"nanoid@npm:^3.1.23, nanoid@npm:^3.1.30": +"nanoid@npm:^3.1.22, nanoid@npm:^3.1.23, nanoid@npm:^3.1.30": version: 3.1.30 resolution: "nanoid@npm:3.1.30" bin: @@ -14027,6 +14028,16 @@ fsevents@~2.3.2: languageName: node linkType: hard +"open@npm:^7.4.2": + version: 7.4.2 + resolution: "open@npm:7.4.2" + dependencies: + is-docker: ^2.0.0 + is-wsl: ^2.1.1 + checksum: 3333900ec0e420d64c23b831bc3467e57031461d843c801f569b2204a1acc3cd7b3ec3c7897afc9dde86491dfa289708eb92bba164093d8bd88fb2c231843c91 + languageName: node + linkType: hard + "open@npm:^8.0.5": version: 8.3.0 resolution: "open@npm:8.3.0" @@ -16906,13 +16917,6 @@ fsevents@~2.3.2: languageName: node linkType: hard -"rollup-plugin-analyzer@npm:^4.0.0": - version: 4.0.0 - resolution: "rollup-plugin-analyzer@npm:4.0.0" - checksum: 72f794f79efe4f620674a48949be9f64dc3cbf601c52ff90ae7cbeaacb604b86bb34321695d0f8f320e5c0b47880022849d92c51d231e1b32c6757a2c627be5e - languageName: node - linkType: hard - "rollup-plugin-dts@npm:^4.0.0": version: 4.0.0 resolution: "rollup-plugin-dts@npm:4.0.0" @@ -16957,6 +16961,22 @@ fsevents@~2.3.2: languageName: node linkType: hard +"rollup-plugin-visualizer@npm:^5.5.2": + version: 5.5.2 + resolution: "rollup-plugin-visualizer@npm:5.5.2" + dependencies: + nanoid: ^3.1.22 + open: ^7.4.2 + source-map: ^0.7.3 + yargs: ^16.2.0 + peerDependencies: + rollup: ^2.0.0 + bin: + rollup-plugin-visualizer: dist/bin/cli.js + checksum: b8a252c25efcf3dbd17557517768acc43208005dc9e3b805c3411dc226dd6765fc9779bf5c91577e909801a83b5f0bc2f6338e5b715f8ca8b4ecc924b12e8f25 + languageName: node + linkType: hard + "rollup-pluginutils@npm:^2.8.2": version: 2.8.2 resolution: "rollup-pluginutils@npm:2.8.2"