文档简介: Next.js 16 现代打包优化完整指南,涵盖代码分割、Tree Shaking、Bundle分析、Turbopack优化等现代打包技术
目标读者: 具备Next.js基础的中高级开发者,需要优化应用性能和打包体积的前端工程师
前置知识: Next.js 16基础、Webpack/Turbopack概念、HTTP协议、性能优化基础、JavaScript模块化
预计时长: 6-10小时
文档信息(用途、难度与维护记录)
| 属性 | 内容 |
|---|---|
| 模块 | 02-nextjs-frontend |
| 分类 | reference |
| 难度 | ⭐⭐⭐ (精通) |
| 标签 | #bundle-optimization #code-splitting #tree-shaking #performance #webpack #turbopack |
| 更新日期 | 2026年9月 |
| 作者 | Dev Quest Team |
| 状态 | ✅ 已完成 |
- 掌握Next.js 16现代化打包工具链和优化策略
- 实现智能代码分割和动态导入,优化初始加载性能
- 运用Tree Shaking消除无用代码,减少打包体积
- 掌握Bundle分析和监控工具,持续优化性能
- 实现现代化的缓存策略和长期缓存机制
- 配置Turbopack获得极致的构建性能
- 实现微前端和模块联邦架构的打包优化
- 掌握服务端组件和客户端组件的打包策略
- 实现现代化的依赖管理和vendor优化
- 掌握字体、图片、静态资源的优化策略
- 实现CDN和边缘计算的资源分发优化
- 构建自动化性能监控和告警系统
为关键页面记录初始下载量、执行成本和交互延迟,性能预算从目标设备和用户网络推导。包大小增加时查看新增了哪个依赖、进入了哪些路由,不只看整个仓库压缩后的总数。
把分析和比较接入构建流程,超过预算时输出可定位的差异与例外说明。多环境构建要确认公开配置与秘密边界,团队规范围绕可重复测量和回归判断,而不是要求每次都采用更多优化工具。
打包优化的目标不是让所有项目使用同一组开关,而是用实际的构建产物和用户路径减少不必要的下载、解析和执行。Next.js 16 默认使用 Turbopack;Webpack 仍可作为兼容或迁移选择。两者的构建时间、缓存命中和输出体积都取决于依赖图、机器、冷/热缓存、路由和配置,因此先建立可复现基线,再只改一个变量进行比较。
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// Next.js 16:Turbopack 是 next dev 与 next build 的默认打包器,
// 配置位于顶层 turbopack 键(不再是 experimental.turbo)
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/turbopack'],
as: '*.js',
},
},
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
}
export default nextConfig如何评估 Turbopack 是否对当前工程有帮助:
| 观察项 | 固定条件 | 比较方法 | 不应推出的结论 |
|---|---|---|---|
| 增量更新时间 | 同一提交、同一机器,分别记录冷启动与热缓存 | 修改一个只被单一路由使用的模块,记录文件保存到页面更新的时间 | 一次快速刷新不代表所有路由或生产构建都会更快 |
| 构建时间与内存 | 固定 Node 版本、锁文件、CPU 并发和缓存状态 | 在相同命令下至少运行三次,报告中位数与范围 | 不能把某个仓库的倍数写成 Turbopack 的通用保证 |
| 产物与请求 | 固定路由、构建模式和分析工具版本 | 比较首个用户路径的 JS/CSS 请求、压缩大小与重复模块 | 构建更快不等于客户端下载更少 |
| TypeScript 行为 | 固定 tsconfig、类型检查命令和项目文件 |
分别运行类型检查与构建,记录任一失败 | 打包器能处理语法不代表项目类型检查已经通过 |
Turbopack 使用依赖图缓存和并行工作来减少不必要的重新计算;实际收益必须由上述基线证明。若项目依赖 Webpack 专用 loader 或插件,先用 next dev --webpack 或 next build --webpack 明确验证迁移路径,而不要同时保留两套不被执行的配置。
// next.config.ts - Webpack 自定义配置
// ⚠️ Next.js 16 中 Webpack 不再是默认打包器:检测到 webpack 配置时
// 必须以 next dev --webpack / next build --webpack 显式启用,否则构建失败
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// 生产环境优化
if (!dev && !isServer) {
config.optimization = {
...config.optimization,
usedExports: true,
sideEffects: false,
splitChunks: {
chunks: 'all',
cacheGroups: {
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10,
chunks: 'all',
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
priority: 20,
chunks: 'all',
},
},
},
}
}
// 追加规则而不覆盖 Next/Webpack 的默认扩展名;去重避免重复解析。
config.resolve.extensions = Array.from(new Set([
...(config.resolve.extensions ?? []),
'.tsx', '.ts', '.jsx', '.js', '.json',
]))
return config
},
}
export default nextConfig// app/dashboard/page.tsx - 自动分割
import { Suspense } from 'react'
import DashboardLayout from '@/components/dashboard/DashboardLayout'
import DashboardOverview from '@/components/dashboard/DashboardOverview'
import RecentActivity from '@/components/dashboard/RecentActivity'
export default function DashboardPage() {
return (
<DashboardLayout>
<Suspense fallback={<div>Loading overview...</div>}>
<DashboardOverview />
</Suspense>
<Suspense fallback={<div>Loading activity...</div>}>
<RecentActivity />
</Suspense>
</DashboardLayout>
)
}// components/lazy/HeavyComponent.tsx
'use client'
import { useState, useEffect } from 'react'
export default function HeavyComponent() {
const [data, setData] = useState(null)
useEffect(() => {
// 模拟重型数据加载
import('@/lib/heavy-computation').then(module => {
const result = module.computeComplexData()
setData(result)
})
}, [])
return (
<div>
<h2>Heavy Component</h2>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
)
}
// pages/heavy-page.tsx
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
// 动态导入重型组件
const HeavyComponent = dynamic(() => import('@/components/lazy/HeavyComponent'), {
loading: () => <div>Loading heavy component...</div>,
ssr: false, // 客户端渲染
})
export default function HeavyPage() {
return (
<div>
<h1>Heavy Page</h1>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
)
}// lib/dynamic-imports.ts
export const loadChartLibrary = () => import('chart.js')
export const loadEditor = () => import('@/components/editor/AdvancedEditor')
export const loadAdminPanel = () => import('@/components/admin/AdminPanel')
// components/charts/DynamicChart.tsx
'use client'
import { useState, useEffect, useRef } from 'react'
interface ChartProps {
type: 'line' | 'bar' | 'pie'
data: any[]
}
export default function DynamicChart({ type, data }: ChartProps) {
const [ChartComponent, setChartComponent] = useState<any>(null)
useEffect(() => {
const loadChart = async () => {
try {
const chartModule = await import('chart.js/auto')
const Chart = chartModule.default
setChartComponent(() => {
return (props: any) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (canvasRef.current) {
const ctx = canvasRef.current.getContext('2d')
new Chart(ctx, {
type,
data: props.data,
options: props.options,
})
}
}, [])
return <canvas ref={canvasRef} />
}
})
} catch (error) {
console.error('Failed to load chart library:', error)
}
}
// 只在需要时加载
if (data.length > 0) {
loadChart()
}
}, [type, data])
if (!ChartComponent) {
return <div className="chart-placeholder">Loading chart...</div>
}
return <ChartComponent data={data} />
}// components/device/DetectDevice.tsx
'use client'
import { useEffect, useState } from 'react'
export default function DetectDevice({ children }: { children: React.ReactNode }) {
const [isMobile, setIsMobile] = useState(false)
const [isTablet, setIsTablet] = useState(false)
useEffect(() => {
const checkDevice = () => {
setIsMobile(window.innerWidth < 768)
setIsTablet(window.innerWidth >= 768 && window.innerWidth < 1024)
}
checkDevice()
window.addEventListener('resize', checkDevice)
return () => window.removeEventListener('resize', checkDevice)
}, [])
// 基于设备类型动态加载组件
if (isMobile) {
return <>{children}</>
}
if (isTablet) {
return <>{children}</>
}
// 桌面端加载更多功能
return <>{children}</>
}
// pages/adaptive-page.tsx
import dynamic from 'next/dynamic'
import DetectDevice from '@/components/device/DetectDevice'
// 桌面端专用组件
const DesktopFeatures = dynamic(
() => import('@/components/desktop/DesktopFeatures'),
{
loading: () => null,
ssr: false
}
)
// 移动端专用组件
const MobileFeatures = dynamic(
() => import('@/components/mobile/MobileFeatures'),
{
loading: () => null,
ssr: false
}
)
export default function AdaptivePage() {
return (
<DetectDevice>
<div>
<h1>Adaptive Content</h1>
<DesktopFeatures />
<MobileFeatures />
</div>
</DetectDevice>
)
}{
"name": "my-nextjs-app",
"sideEffects": [
"**/*.css",
"**/*.scss",
"**/*.module.css",
"**/*.module.scss",
"./src/components/**/*.tsx",
"./src/styles/**/*"
],
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
},
"./components": {
"import": "./dist/components/index.esm.js",
"require": "./dist/components/index.cjs.js"
}
}
}// lib/utils.ts - ES模块写法
// ✅ 正确:具名导出,支持Tree Shaking
export const formatDate = (date: Date): string => {
return date.toLocaleDateString()
}
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout
return (...args: Parameters<T>) => {
clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}
}
export const throttle = <T extends (...args: any[]) => any>(
func: T,
limit: number
): ((...args: Parameters<T>) => void) => {
let inThrottle: boolean
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
// ❌ 错误:默认导出整个库,无法Tree Shaking
export default {
formatDate,
debounce,
throttle
}
// 使用方式 - 支持Tree Shaking
import { formatDate, debounce } from '@/lib/utils'// lib/optimized-imports.ts
// ✅ 正确:按需导入大型库
import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth'
import { collection, query, where, getDocs } from 'firebase/firestore'
import { Button } from '@/components/ui/button' // 按需导入UI组件
import { format } from 'date-fns' // 只导入需要的函数
// ❌ 错误:导入整个库
import firebase from 'firebase'
import * as firebase from 'firebase'
import { DatePicker, TimePicker, Calendar } from 'antd' // 导入未使用的组件
// Lodash 按需导入示例
import { debounce, throttle, cloneDeep } from 'lodash-es'
// 或者使用更小的替代库
import { debounce } from 'just-debounce-it'
import { throttle } from 'just-throttle'
// 图标库按需导入
import {
HomeIcon,
UserIcon,
SettingsIcon
} from '@heroicons/react/24/outline'
// 或者使用更小的图标库
import {
home,
user,
settings
} from 'lucide-react'// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// ... 其他配置
}
module.exports = withBundleAnalyzer(nextConfig)// package.json scripts
{
"scripts": {
"analyze": "ANALYZE=true next build",
"build:analyze": "npm run analyze"
}
}// scripts/analyze-bundle.js
const fs = require('fs')
const path = require('path')
function analyzeBundle(buildDir = '.next') {
const manifestPath = path.join(buildDir, 'build-manifest.json')
if (!fs.existsSync(manifestPath)) {
console.error('Build manifest not found. Run `next build` first.')
return
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
const stats = {
totalFiles: 0,
totalSize: 0,
files: [],
largestFiles: [],
dependencies: new Set(),
}
// 分析页面文件
Object.entries(manifest.pages).forEach(([page, files]) => {
files.forEach(file => {
const filePath = path.join(buildDir, file)
if (fs.existsSync(filePath)) {
const fileStats = fs.statSync(filePath)
const size = fileStats.size
stats.totalFiles++
stats.totalSize += size
stats.files.push({
page,
file,
size: formatBytes(size),
sizeBytes: size,
})
}
})
})
// 找出最大的文件
stats.largestFiles = stats.files
.sort((a, b) => b.sizeBytes - a.sizeBytes)
.slice(0, 10)
// 输出分析结果
console.log('📊 Bundle Analysis Report')
console.log('========================')
console.log(`Total Files: ${stats.totalFiles}`)
console.log(`Total Size: ${formatBytes(stats.totalSize)}`)
console.log('')
console.log('🔍 Largest Files:')
stats.largestFiles.forEach((file, index) => {
console.log(`${index + 1}. ${file.file} (${file.size}) - ${file.page}`)
})
return stats
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 运行分析
analyzeBundle()// scripts/bundle-size-monitor.js
const fs = require('fs')
const path = require('path')
const BUDGET_LIMITS = {
'pages/_app.js': 100 * 1024, // 100KB
'pages/_document.js': 50 * 1024, // 50KB
'pages/index.js': 150 * 1024, // 150KB
'framework.js': 200 * 1024, // 200KB
'main.js': 100 * 1024, // 100KB
}
function checkBundleSize(buildDir = '.next') {
const manifestPath = path.join(buildDir, 'build-manifest.json')
if (!fs.existsSync(manifestPath)) {
console.error('Build manifest not found')
process.exit(1)
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
let warnings = 0
let errors = 0
console.log('🔍 Bundle Size Check')
console.log('===================')
// 检查每个页面的大小
Object.entries(manifest.pages).forEach(([page, files]) => {
files.forEach(file => {
const filePath = path.join(buildDir, file)
if (fs.existsSync(filePath)) {
const fileStats = fs.statSync(filePath)
const size = fileStats.size
// 检查预算限制
const budgetLimit = BUDGET_LIMITS[file] || BUDGET_LIMITS[path.basename(file)]
if (budgetLimit) {
const percentage = (size / budgetLimit * 100).toFixed(1)
const status = percentage > 100 ? '🚨' : percentage > 80 ? '⚠️' : '✅'
console.log(`${status} ${file}: ${formatBytes(size)} (${percentage}% of limit)`)
if (percentage > 100) {
errors++
console.error(` ❌ Exceeds budget limit of ${formatBytes(budgetLimit)}`)
} else if (percentage > 80) {
warnings++
console.warn(` ⚠️ Approaching budget limit`)
}
} else {
console.log(`ℹ️ ${file}: ${formatBytes(size)}`)
}
}
})
})
console.log('')
console.log(`Summary: ${errors} errors, ${warnings} warnings`)
if (errors > 0) {
console.error('❌ Bundle size check failed')
process.exit(1)
} else {
console.log('✅ Bundle size check passed')
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
checkBundleSize()// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 启用内容哈希
generateEtags: true,
// 静态文件优化
experimental: {
optimizeCss: true,
optimizePackageImports: ['@heroicons/react', 'lucide-react']
},
// 输出配置
distDir: '.next',
// 启用压缩
compress: true,
// 图片优化
images: {
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
module.exports = nextConfig// lib/cache-strategies.ts
export const cacheConfig = {
// 静态资源长期缓存
staticAssets: {
maxAge: 365 * 24 * 60 * 60, // 1年
immutable: true,
},
// HTML文件短期缓存
pages: {
maxAge: 24 * 60 * 60, // 1天
mustRevalidate: true,
},
// API响应缓存
api: {
maxAge: 5 * 60, // 5分钟
staleWhileRevalidate: 60, // 1分钟
},
}
// proxy.ts(Next.js 16:由 middleware.ts 更名,运行于 Node.js runtime)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
// 静态资源缓存头
if (request.nextUrl.pathname.match(/\.(css|js|png|jpg|jpeg|gif|webp|svg|woff|woff2)$/)) {
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable')
}
// 页面缓存
if (request.nextUrl.pathname.endsWith('.html') || request.nextUrl.pathname === '/') {
response.headers.set('Cache-Control', 'public, max-age=86400, must-revalidate')
}
return response
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization = {
...config.optimization,
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: 25,
minSize: 20000,
maxSize: 244000,
cacheGroups: {
// React相关
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
priority: 30,
chunks: 'all',
},
// React生态
reactLibs: {
test: /[\\/]node_modules[\\/](@react|react-)[\\/]/,
name: 'react-libs',
priority: 20,
chunks: 'all',
},
// UI库
ui: {
test: /[\\/]node_modules[\\/](@radix-ui|@headlessui|@mui|@chakra-ui|framer-motion)[\\/]/,
name: 'ui-libs',
priority: 15,
chunks: 'all',
},
// 工具库
utils: {
test: /[\\/]node_modules[\\/](lodash|date-fns|axios|zustand)[\\/]/,
name: 'utils',
priority: 10,
chunks: 'all',
},
// 其他vendor
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10,
chunks: 'all',
},
},
},
}
}
return config
},
}
module.exports = nextConfig// lib/dynamic-vendor.ts
export const loadChartJs = () => import('chart.js')
export const loadMonacoEditor = () => import('monaco-editor')
export const loadPdfLib = () => import('pdf-lib')
export const loadExcelJs = () => import('exceljs')
// components/admin/ReportGenerator.tsx
'use client'
import { useState } from 'react'
export default function ReportGenerator() {
const [excelLib, setExcelLib] = useState<any>(null)
const [pdfLib, setPdfLib] = useState<any>(null)
const loadLibraries = async () => {
try {
// 按需加载重型库
const [excelModule, pdfModule] = await Promise.all([
import('exceljs'),
import('pdf-lib')
])
setExcelLib(excelModule.Workbook)
setPdfLib(pdfModule.PDFDocument)
} catch (error) {
console.error('Failed to load libraries:', error)
}
}
return (
<div>
<button onClick={loadLibraries}>
Load Report Libraries
</button>
{excelLib && pdfLib && (
<div>
<p>Libraries loaded successfully!</p>
{/* 使用库生成报告 */}
</div>
)}
</div>
)
}// components/images/OptimizedImage.tsx
import Image from 'next/image'
import { useState } from 'react'
interface OptimizedImageProps {
src: string
alt: string
width?: number
height?: number
priority?: boolean
fill?: boolean
className?: string
}
export default function OptimizedImage({
src,
alt,
width,
height,
priority = false,
fill = false,
className,
}: OptimizedImageProps) {
const [isLoading, setIsLoading] = useState(true)
return (
<div className={`relative ${className || ''}`}>
<Image
src={src}
alt={alt}
width={width}
height={height}
fill={fill}
priority={priority}
className={`
duration-700 ease-in-out
${isLoading ? 'scale-110 blur-2xl grayscale' : 'scale-100 blur-0 grayscale-0'}
`}
onLoadingComplete={() => setIsLoading(false)}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
)
}
// 使用示例
export function ProductGallery({ images }: { images: string[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{images.map((image, index) => (
<OptimizedImage
key={index}
src={image}
alt={`Product image ${index + 1}`}
width={400}
height={300}
priority={index === 0} // 第一张图片优先加载
/>
))}
</div>
)
}// components/images/ResponsiveImage.tsx
import Image from 'next/image'
interface ResponsiveImageProps {
src: string
alt: string
aspectRatio?: number
priority?: boolean
}
export default function ResponsiveImage({
src,
alt,
aspectRatio = 16 / 9,
priority = false,
}: ResponsiveImageProps) {
return (
<div className="relative w-full" style={{ aspectRatio }}>
<Image
src={src}
alt={alt}
fill
priority={priority}
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 75vw,
50vw
"
style={{
objectFit: 'cover',
}}
/>
</div>
)
}
// next.config.js - 图片优化配置
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
// 启用现代格式
formats: ['image/webp', 'image/avif'],
// 响应式断点
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// 外部图片域名(Next.js 16:images.domains 已弃用,改用 remotePatterns)
remotePatterns: [
{ protocol: 'https', hostname: 'example.com' },
{ protocol: 'https', hostname: 'cdn.example.com' },
],
// 图片质量(Next 16 起 quality 单值改为 qualities 数组)
qualities: [85],
// 最小化缓存时间
minimumCacheTTL: 60,
},
}
module.exports = nextConfig// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap', // 字体交换策略
preload: true, // 预加载
weight: ['400', '500', '600', '700'], // 只加载需要的字重
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={inter.variable}>
<body className={inter.className}>
{children}
</body>
</html>
)
}
// lib/font-utils.ts
export const fontDisplayStrategies = {
swap: 'font-display: swap;', // 字体交换
block: 'font-display: block;', // 阻塞显示
fallback: 'font-display: fallback;', // 回退
optional: 'font-display: optional;', // 可选
}
export const generateFontFace = (fontFamily: string, src: string, display = 'swap') => `
@font-face {
font-family: '${fontFamily}';
src: ${src};
font-display: ${display};
font-weight: normal;
font-style: normal;
}
`// scripts/subset-fonts.js
const fontkit = require('fontkit')
const fs = require('fs')
async function subsetFont(inputPath, outputPath, characters) {
const font = fontkit.open(fs.readFileSync(inputPath))
const subset = font.createSubset()
// 添加字符到子集
characters.split('').forEach(char => {
subset.addGlyph(char.charCodeAt(0))
})
// 生成子集字体
const subsetBuffer = subset.encode()
fs.writeFileSync(outputPath, subsetBuffer)
console.log(`Font subset created: ${outputPath}`)
}
// 常用字符集
const commonCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'
// 子集化字体文件
async function optimizeFonts() {
await subsetFont(
'./fonts/Inter-Regular.ttf',
'./fonts/Inter-Regular-Subset.ttf',
commonCharacters
)
await subsetFont(
'./fonts/Inter-Bold.ttf',
'./fonts/Inter-Bold-Subset.ttf',
commonCharacters
)
}
optimizeFonts()// lib/bundle-monitor.ts
export interface BundleMetrics {
name: string
size: number
gzippedSize: number
loadTime: number
}
class BundleMonitor {
private metrics: Map<string, BundleMetrics> = new Map()
// 记录Bundle加载性能
recordBundleLoad(name: string, loadTime: number) {
const metric = this.metrics.get(name) || { name, size: 0, gzippedSize: 0, loadTime: 0 }
metric.loadTime = loadTime
this.metrics.set(name, metric)
}
// 获取Bundle大小信息
async getBundleSize(url: string): Promise<number> {
try {
const response = await fetch(url, { method: 'HEAD' })
return parseInt(response.headers.get('content-length') || '0')
} catch (error) {
console.error('Failed to get bundle size:', error)
return 0
}
}
// 生成性能报告
generateReport(): BundleMetrics[] {
return Array.from(this.metrics.values()).sort((a, b) => b.size - a.size)
}
// 检查性能预算
checkBudget(budget: { [key: string]: number }): { passed: boolean; violations: string[] } {
const violations: string[] = []
this.metrics.forEach((metric, name) => {
const limit = budget[name] || budget.default || 100 * 1024 // 默认100KB
if (metric.size > limit) {
violations.push(`${name}: ${this.formatBytes(metric.size)} exceeds ${this.formatBytes(limit)}`)
}
})
return {
passed: violations.length === 0,
violations
}
}
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
}
export const bundleMonitor = new BundleMonitor()
// components/BundleMonitor.tsx
'use client'
import { useEffect, useState } from 'react'
import { bundleMonitor, BundleMetrics } from '@/lib/bundle-monitor'
export default function BundleMonitorDashboard() {
const [metrics, setMetrics] = useState<BundleMetrics[]>([])
const [budget, setBudget] = useState({
'main.js': 100 * 1024,
'vendors.js': 200 * 1024,
default: 50 * 1024,
})
useEffect(() => {
// 监控Bundle加载
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.includes('.js')) {
bundleMonitor.recordBundleLoad(
entry.name.split('/').pop()!,
entry.duration
)
setMetrics(bundleMonitor.generateReport())
}
})
})
observer.observe({ entryTypes: ['resource'] })
return () => observer.disconnect()
}, [])
const budgetCheck = bundleMonitor.checkBudget(budget)
return (
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">Bundle Performance Monitor</h2>
{!budgetCheck.passed && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 rounded">
<h3 className="font-semibold text-red-800">Budget Violations:</h3>
<ul className="list-disc list-inside text-red-700">
{budgetCheck.violations.map((violation, index) => (
<li key={index}>{violation}</li>
))}
</ul>
</div>
)}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Bundle
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Size
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Load Time
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{metrics.map((metric) => (
<tr key={metric.name}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{metric.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatBytes(metric.size)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{metric.loadTime.toFixed(2)}ms
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}// webpack.config.js (主应用)
const ModuleFederationPlugin = require('@module-federation/webpack')
module.exports = {
webpack: (config) => {
config.plugins.push(
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
remotes: {
dashboard: 'dashboard@http://localhost:3001/remoteEntry.js',
admin: 'admin@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
'next': { singleton: true, requiredVersion: '^15.0.0' },
},
})
)
return config
},
}
// components/Dashboard.tsx
import dynamic from 'next/dynamic'
// 动态加载远程模块
const RemoteDashboard = dynamic(() => import('dashboard/Dashboard'), {
loading: () => <div>Loading Dashboard...</div>,
ssr: false,
})
export default function ShellApp() {
return (
<div>
<header>Shell Application</header>
<main>
<RemoteDashboard />
</main>
</div>
)
}// app/components/ServerComponent.tsx
import { cache } from 'react'
// 缓存数据获取函数
const getPosts = cache(async (category?: string) => {
const url = category
? `https://api.example.com/posts?category=${category}`
: 'https://api.example.com/posts'
const response = await fetch(url, {
next: {
revalidate: 3600, // 1小时缓存
tags: ['posts', category || 'all']
}
})
return response.json()
})
// 服务端组件
export default async function ServerComponent({ category }: { category?: string }) {
const posts = await getPosts(category)
return (
<div>
<h1>{category || 'All'} Posts</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
// 流式加载
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<ServerComponent />
</Suspense>
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
</div>
)
}先确认当前构建器,再使用它支持的配置;Turbopack 与 Webpack 的选项不能直接混用。动态导入可以减少首屏工作,却可能让用户点击后等待,应检查实际网络瀑布与交互时间。
服务端组件可减少部分客户端代码,客户端仍需承担交互和相关依赖。移除未用依赖、控制重复包和按需加载后复测关键页面。微前端、边缘部署与 CDN 各有组织或交付目标,不应被列为每个应用都要勾选的打包优化步骤。
- Webpack Bundle Analyzer: 可视化Bundle分析
- Lighthouse: 综合性能评估
- Bundlephobia: 依赖包大小分析
- Next.js Bundle Analyzer: Next.js专用分析工具
- PurgeCSS: 移除未使用的CSS
- Imagemin: 图片压缩优化
- Terser: JavaScript压缩
- CSS Nano: CSS优化
- Sentry: 错误和性能监控
- Vercel Analytics: Next.js性能分析
- SpeedCurve: 性能趋势监控
- Calibre: 持续性能监控
Next.js 16 的打包优化生态系统为现代Web应用提供了完整的优化解决方案。通过合理运用代码分割、Tree Shaking、缓存策略和监控工具,可以构建出高性能、用户友好的现代化应用。
- 📖 Turbopack配置: 革命性打包工具配置
- 📖 代码分割策略: 智能分割和懒加载
- 📖 Tree Shaking优化: 死代码消除和依赖优化
- 📖 Bundle分析: 打包分析和性能监控
- 📖 高级优化技术: 微前端和模块联邦
- 工具按兼容性与证据选择:先记录目标框架版本、插件约束、生产构建与调试需求,再选择默认打包器或兼容方案;“更快”必须指明冷启动、热更新还是生产构建。
- 分割改变请求形状:路由或动态组件分割后检查目标页面的初始资源、额外请求瀑布与交互等待。更小首包可能换来更多网络往返,不能只比较一个文件大小。
- Tree shaking 依赖模块边界:检查生产产物而非源码 import 数量;副作用导入、CommonJS 包装和错误的 barrel 导出都可能保留不需要的代码。
- 缓存必须能回退与失效:静态资源使用内容标识,HTML、数据与用户授权数据按各自有效期处理;发布后确认旧客户端不会请求不存在的 chunk。
- 预算由旅程驱动:为关键路由设定可审查的资源和交互预算,持续记录 build、设备与网络条件。预算报警是复查入口,不是自动证明用户体验良好。
- 保存一个关键路由优化前后的生产构建分析,列出减少或新增的资源及其加载时机。
- 在固定网络条件下验证代码分割前后首屏与交互的等待,说明取舍而非只报告首包大小。
- 故意引入副作用导入或大依赖,确认产物报告能发现回归;修复后记录差异。
- 在一次测试发布后访问旧页面或模拟旧 HTML,确认内容哈希资源和回退策略不会造成 chunk 加载失败。
- 把一个预算阈值关联到具体路由和测量环境,发生超标后能追溯到依赖或代码改动。
如果你发现本文档有改进空间,欢迎:
- 🐛 报告问题: 在Issues中提出具体问题
- 💡 建议改进: 提出修改建议和补充内容
- 📝 参与贡献: 提交PR完善文档内容
分享你的学习体验:
- ✅ 有用内容: 哪些部分对你最有帮助
- ❓ 疑问点: 哪些内容需要进一步澄清
- 🎯 建议: 希望增加什么内容
- Next.js: Build Optimization - Next.js构建优化
- Webpack: Optimization - Webpack优化指南
- Turbopack: Official Documentation - Rust编写的增量打包器
- Bundle Analyzer: webpack-bundle-analyzer - 打包分析工具
- Bundlephobia: 依赖包大小分析 - NPM包体积分析
- Module Federation: 微前端架构 - 模块联邦文档
- Performance Budgets: 性能预算工具 - 性能预算设置
- Chrome DevTools: Coverage Analysis - 代码覆盖率分析
文档状态: ✅ 已完成 | 🚧 进行中 | 📋 计划中 最后更新: 2026年9月 版本: v1.0.0