Skip to content

第14章 代码分割与产物优化

开篇引言

构建工具的最终产物质量,直接决定了用户在浏览器中的加载体验。Vite 在这一环节投入了大量的工程设计:从代码分割策略到 Tree Shaking,从多种压缩引擎的选择到 License 合规提取,从 Manifest 文件的精确映射到 Module Preload 的智能预加载,这些机制共同构成了 Vite 的产物优化体系。

本章将从 Vite 源码出发,逐一剖析这些优化手段的设计原理与实现细节。通过对 terser.tslicense.tsmanifest.tsimportAnalysisBuild.tsmodulePreloadPolyfill.ts 等核心文件的深度解读,我们将理解 Vite 如何在构建阶段将开发者的源代码转化为高性能的生产产物。

本章要点

  • 理解 Vite 基于 Rolldown 的代码分割策略及 chunk 生成逻辑
  • 掌握 Tree Shaking 在 ESM 语义下的工作机制与 preserve_annotations 的设计
  • 深入 Terser 多线程压缩架构与 WorkerWithFallback 惰性初始化模式
  • 分析 License 提取插件的依赖遍历与合规输出设计
  • 理解 Manifest 文件生成的三层插件协作架构
  • 掌握 Module Preload 的并行预加载策略与 Polyfill 注入机制

14.1 代码分割策略

14.1.1 从入口到 Chunk 的拓扑分析

代码分割(Code Splitting)是现代 Web 构建的核心优化手段。Vite 的代码分割建立在 Rolldown 的 chunk 图分析之上,其基本原则是:

  1. 每个入口产生一个 chunk:静态入口点各自生成独立的输出 chunk
  2. 动态导入产生异步 chunkimport() 表达式的目标模块被分割为独立的异步 chunk
  3. 公共模块提取:多个 chunk 共同依赖的模块被提取到共享 chunk 中

Vite 在 build.ts 中通过 Rolldown 的 preserveEntrySignatures 配置控制入口签名的保留策略。对于应用模式,该选项默认设为 false,允许 Rolldown 自由进行 chunk 合并优化:

typescript
const bundle = await rolldown({
  ...rollupOptions,
  input,
  preserveEntrySignatures: false,
  // ...
})

preserveEntrySignatures 设为 false 时,Rolldown 可以将入口 chunk 中的部分代码拆分到共享 chunk 中。这意味着入口模块的原始导出签名可能不再完全保留在单个 chunk 中,但换来的是更优的代码去重和更小的总体积。

14.1.2 CSS 代码分割

CSS 代码分割是 Vite 的一个重要特性。当 build.cssCodeSplittrue(默认值)时,异步 chunk 引用的 CSS 会被提取为独立的 CSS 文件,并在运行时通过动态 <link> 标签加载。

这种并行加载策略避免了传统方案中"先加载 JS 再发现 CSS 依赖"的瀑布流问题。Vite 通过 viteMetadata 在 chunk 上标注其关联的 CSS 和静态资源:

typescript
chunk.viteMetadata!.importedCss.forEach((file) => {
  mappedChunks.push(joinUrlSegments(base, file))
})
chunk.viteMetadata!.importedAssets.forEach((file) => {
  mappedChunks.push(joinUrlSegments(base, file))
})

viteMetadata 是 Vite 扩展的 Rolldown chunk 元数据,它在构建过程中由 CSS 插件和资源插件填充,记录了每个 JS chunk 关联的所有 CSS 文件和静态资源文件。这个元数据在 Manifest 生成、SSR Manifest 生成、Module Preload 注入等多个场景中被复用。

14.1.3 manualChunks 与自定义分割

Vite 允许通过 build.rollupOptions.output.manualChunks 进行自定义分割。这在需要将特定的第三方库(如 Vue、React)提取为独立 chunk 以优化缓存命中率时特别有用:

typescript
// vite.config.ts 示例
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return 'vendor'
          }
        }
      }
    }
  }
}

manualChunks 函数接收每个模块的 ID,返回该模块应属于的 chunk 名称。Rolldown 会将返回相同名称的所有模块合并到同一个 chunk 中。需要注意的是,不当的 manualChunks 配置可能导致循环 chunk 依赖或代码冗余,因此在生产中需要结合实际的模块依赖图谨慎使用。

14.1.4 库模式的特殊处理

build.lib 配置存在时,Vite 进入库模式。库模式下的代码分割策略与应用模式有显著不同:

  • 默认不进行代码分割,将所有代码打包到单个文件
  • 支持同时输出 ES 和 CJS 两种格式
  • external 配置决定哪些依赖不被打包

这种差异化策略确保了库产物的可预测性和兼容性。

14.2 Tree Shaking

14.2.1 ESM 语义与静态分析

Tree Shaking 的核心在于 ESM 的静态结构特性。ESM 的 import/export 语句在编译时即可确定模块间的依赖关系,这使得构建工具能够在不执行代码的情况下精确标记未被使用的导出。

Vite/Rolldown 的 Tree Shaking 流程可以概括为以下几个阶段:

sideEffects 字段来自 package.json,它告诉构建工具该包中的模块是否具有副作用。当一个模块被标记为无副作用且其导出未被引用时,整个模块可以被安全移除。这对于像 lodash-es 这样的工具库尤为重要 -- 如果应用只使用了 debounce 函数,其他数百个未使用的函数将被完全移除。

14.2.2 Pure Annotation 保留

一个精妙的设计细节出现在 Terser 插件中。当构建目标是 ES 格式的库时,Vite 会强制保留 pure annotation(纯注释标记):

typescript
// terser.ts - terserPlugin 中的关键逻辑
const res = await worker.run(terserPath, code, {
  safari10: true,
  ...terserOptions,
  format: {
    ...terserOptions.format,
    // 对于 ES 库模式,保留 pure annotations 以支持下游 Tree Shaking
    preserve_annotations:
      config.build.lib && outputOptions.format === 'es'
        ? true
        : terserOptions.format?.preserve_annotations,
  },
  sourceMap: !!outputOptions.sourcemap,
  module: outputOptions.format.startsWith('es'),
  toplevel: outputOptions.format === 'cjs',
})

/*#__PURE__*/ 注释告诉下游的 Tree Shaker 某个函数调用是无副作用的,可以安全移除。当 Vite 构建的是一个库时,压缩阶段必须保留这些注释,否则下游消费者将失去对这些调用进行 Tree Shaking 的能力。这是一个跨构建工具链的协作设计 -- Vite 的产物作为另一个构建工具的输入,两者通过注释约定实现协同优化。

14.2.3 sideEffects 与模块级标记

除了函数级的 /*#__PURE__*/ 标记,模块级的 sideEffects 声明也是 Tree Shaking 的重要输入。Rolldown 在模块解析阶段读取每个包的 package.json 中的 sideEffects 字段:

  • "sideEffects": false -- 包中所有文件都没有副作用
  • "sideEffects": ["*.css", "*.global.js"] -- 仅指定文件有副作用
  • 不声明 -- 默认所有文件可能有副作用

这两级标记体系(模块级 + 表达式级)构成了 Tree Shaking 的完整判断依据。

14.3 Terser 压缩引擎

14.3.1 多线程压缩架构

Vite 的 Terser 压缩插件(plugins/terser.ts)采用了一种精巧的多线程架构。核心在于 WorkerWithFallback 的使用,它来自 artichokie 库:

typescript
const makeWorker = () =>
  new WorkerWithFallback(
    () =>
      async (
        terserPath: string,
        code: string,
        options: TerserMinifyOptions,
      ) => {
        // 在 Worker 线程中动态 import terser
        const terser: typeof import('terser') = await import(terserPath)
        try {
          return (await terser.minify(code, options)) as TerserMinifyOutput
        } catch (e) {
          // 错误对象的额外属性在线程间传递时会丢失
          // 需要手动提取 stack 并展开
          throw { stack: e.stack, ...e }
        }
      },
    {
      shouldUseFake(_terserPath, _code, options) {
        // 当选项包含不可序列化的函数时,回退到主线程
        return !!(
          (typeof options.mangle === 'object' &&
            (options.mangle.nth_identifier?.get ||
              (typeof options.mangle.properties === 'object' &&
                options.mangle.properties.nth_identifier?.get))) ||
          typeof options.format?.comments === 'function' ||
          typeof options.output?.comments === 'function' ||
          options.nameCache
        )
      },
      max: maxWorkers,
    },
  )

这个设计体现了三个关键决策:

惰性加载 (Lazy Loading):Terser 是可选依赖,仅在实际需要压缩时才动态加载。terserPath 通过 nodeResolveWithVite 先从项目根目录查找,再从 Vite 安装路径查找。Worker 本身也是惰性创建的,通过 worker ||= makeWorker() 确保只在第一个 chunk 需要压缩时才初始化。

序列化降级 (Serialization Fallback):Worker 线程间通信依赖结构化克隆算法,函数无法被序列化。当用户配置了不可序列化的选项(如自定义 comments 过滤函数、nth_identifier 生成器、nameCache 对象),系统通过 shouldUseFake 检测并自动降级到主线程执行。这种"能并行则并行,不能则降级"的策略在高性能工具中非常常见。

Worker 池复用worker 变量在插件生命周期内保持单例。WorkerWithFallback 内部维护了一个有上限的线程池(默认 CPU 核心数 - 1),多个 chunk 的压缩任务会被自动调度到空闲的 Worker 上。在 closeBundle Hook 中,Worker 池被正确释放。

14.3.2 错误增强与定位

Terser 压缩阶段的错误处理也值得关注。当压缩失败时,Vite 会增强错误信息,添加精确的文件位置和代码帧:

typescript
catch (e) {
  if (e.line !== undefined && e.col !== undefined) {
    e.loc = {
      file: chunk.fileName,
      line: e.line,
      column: e.col,
    }
  }
  if (e.pos !== undefined) {
    e.frame = generateCodeFrame(code, e.pos)
  }
  throw e
}

generateCodeFrame 是 Vite 的通用工具函数,它从源代码中提取错误位置周围的代码片段,并用下划线标记出具体位置。这种错误增强模式在 Vite 的多个插件中被复用,为开发者提供了一致的调试体验。

14.3.3 构建目标适配

Terser 的行为会根据输出格式和构建目标自动调整:

配置项条件效果
module: true输出格式以 es 开头启用 ES Module 模式,保留 import/export
toplevel: true输出格式为 cjs允许混淆顶层变量名
safari10: true始终开启修复 Safari 10/11 的已知语法 bug
preserve_annotations库模式 + ES 格式保留 /*#__PURE__*/ 注释

14.3.4 applyToEnvironment 的环境过滤

Terser 插件通过 applyToEnvironment 实现精确的环境匹配:

typescript
applyToEnvironment(environment) {
  // 即使 minify 不是 'terser',也需要保留该插件
  // 因为 plugin-legacy 会强制使用 terser 处理 legacy chunks
  return !!environment.config.build.minify
}

这段注释揭示了一个重要的设计约束:即使用户选择了 esbuild 或 oxc 作为默认压缩器,Terser 插件仍需保持激活状态,以便 @vitejs/plugin-legacy 在处理遗留兼容 chunk 时能够使用 Terser。

14.4 License 提取

14.4.1 合规性自动化

在生产构建中,正确处理第三方依赖的许可证信息是一项法律合规要求。许多开源协议(如 MIT、Apache 2.0)要求在分发衍生作品时保留原始版权声明。Vite 的 license.ts 插件通过 generateBundle Hook 自动收集并输出许可证文件,将这项繁琐的人工任务自动化。

核心实现的关键路径:

typescript
for (const file in bundle) {
  const chunk = bundle[file]
  if (chunk.type === 'asset') continue  // 跳过资源文件

  for (const moduleId of chunk.moduleIds) {
    // 跳过虚拟模块(以 \0 开头)和非 node_modules 模块
    if (moduleId.startsWith('\0') || !isInNodeModules(moduleId)) continue

    // 查找最近的主 package.json(跳过嵌套包的内部 package.json)
    const pkgData = findNearestMainPackageData(
      path.dirname(moduleId),
      packageCache,
    )
    if (!pkgData) continue

    const { name, version = '0.0.0', license } = pkgData.data
    const key = `${name}@${version}`
    if (licenses[key]) continue  // 同一包的同一版本只记录一次

    const entry: LicenseEntry = { name, version }
    if (license) {
      entry.identifier = license.trim()  // SPDX 标识符
    }
    const licenseFile = findLicenseFile(pkgData.dir)
    if (licenseFile) {
      entry.text = fs.readFileSync(licenseFile, 'utf-8').trim()
    }
    licenses[key] = entry
  }
}

几个值得关注的设计点:

  1. findNearestMainPackageData 而非普通的 findNearestPackageData:这确保了找到的是包的主 package.json,而非子目录中可能存在的辅助 package.json
  2. name@version 去重:同一个包在多个 chunk 中被引用时,许可证信息只记录一次
  3. packageCache 复用:避免对同一目录反复读取文件系统

14.4.2 许可证文件查找策略

findLicenseFile 函数遵循 npm 的标准约定:

typescript
// 参考 npm-packlist 的许可证文件匹配规则
const licenseFiles = [/^license/i, /^licence/i, /^copying/i]

function findLicenseFile(pkgDir: string) {
  const files = fs.readdirSync(pkgDir)
  const matchedFile = files.find((file) =>
    licenseFiles.some((re) => re.test(file)),
  )
  if (matchedFile) {
    return path.join(pkgDir, matchedFile)
  }
}

这个设计兼容了 LICENSELICENSE.mdLICENSE.txtLICENCE(英式拼写)、COPYING(GNU 风格)以及 COPYING.md 等各种命名约定。正则使用了 ^ 锚定和 i 不区分大小写标志,确保只匹配文件名开头部分。

14.4.3 输出格式

License 插件支持两种输出格式,由 fileName 配置的后缀决定:

Markdown 格式(默认 .vite/license.md):

typescript
function licenseEntryToMarkdown(licenses: LicenseEntry[]) {
  let text = `# Licenses\n\nThe app bundles dependencies which contain the following licenses:\n`
  for (const license of licenses) {
    const nameAndVersionText = `${license.name} - ${license.version}`
    const identifierText = license.identifier ? ` (${license.identifier})` : ''
    text += `\n## ${nameAndVersionText}${identifierText}\n`
    if (license.text) {
      text += `\n${license.text}\n`
    }
  }
  return text
}

JSON 格式(当文件名以 .json 结尾时):适合程序化处理,可用于构建合规报告或自动化审计。

14.4.4 许可证排序

输出前,许可证条目会通过 sortObjectKeys(licenses) 进行排序。这确保了构建产物的确定性 -- 无论模块被遍历的顺序如何,最终输出的许可证列表始终保持字母序,方便版本控制系统进行 diff 比较。

14.5 Manifest 生成

14.5.1 三层插件协作架构

Manifest 插件(plugins/manifest.ts)的设计展示了 Vite 如何在 Rolldown 原生实现与 Vite 兼容层之间协作。它通过 perEnvironmentPlugin 返回一个由三个插件组成的数组:

第一层:环境引用捕获

typescript
{
  name: 'native:manifest-envs',
  buildStart() {
    envs[environment.name] = this.environment
  },
}

buildStart 时捕获当前环境引用,以便后续插件在 generateBundle 中访问环境特定的 cssEntriesMap

第二层:Rolldown 原生 Manifest 生成

rolldown/experimental 提供的 nativeManifestPlugin,负责基础的 manifest JSON 结构生成。它处理 chunk 的入口信息、文件名映射、静态导入和动态导入关系。

第三层:Vite 兼容层补充

typescript
{
  name: 'native:manifest-compatible',
  generateBundle(_, bundle) {
    const asset = bundle[outPath]
    if (asset.type === 'asset') {
      let manifest: Manifest | undefined
      for (const output of Object.values(bundle)) {
        const importedCss = output.viteMetadata?.importedCss
        const importedAssets = output.viteMetadata?.importedAssets
        if (!importedCss?.size && !importedAssets?.size) continue
        manifest ??= JSON.parse(asset.source.toString()) as Manifest
        // 补充 CSS 和资源映射
        if (output.type === 'chunk') {
          const item = manifest[getChunkName(output)]
          if (item) {
            if (importedCss?.size) item.css = [...importedCss]
            if (importedAssets?.size) item.assets = [...importedAssets]
          }
        }
      }
    }
  },
}

这一层读取 Rolldown 生成的基础 manifest,然后补充 viteMetadata 中的 CSS 和资源映射信息。viteMetadata 是 Vite 在构建过程中通过 CSS 插件和资源插件收集的元数据,Rolldown 的原生插件尚不了解这些信息。

14.5.2 Manifest 数据结构

Manifest 文件是一个 JSON 对象,以模块的原始路径为键,ManifestChunk 为值:

typescript
export interface ManifestChunk {
  src?: string              // 输入文件名
  file: string              // 输出文件名(含 hash)
  css?: string[]            // 关联的 CSS 文件列表
  assets?: string[]         // 关联的资源文件列表(不含 CSS)
  isEntry?: boolean         // 是否为静态入口
  name?: string             // chunk 名称
  isDynamicEntry?: boolean  // 是否为动态入口
  imports?: string[]        // 静态导入的其他 manifest 键
  dynamicImports?: string[] // 动态导入的其他 manifest 键
}

这个数据结构使得后端框架(如 Laravel、Rails)能够根据原始文件路径查找对应的产物文件名,实现正确的 <script><link> 标签注入。

14.5.3 per-environment 状态管理

Manifest 插件使用 perEnvironmentState 实现每环境独立的状态:

typescript
const getState = perEnvironmentState(() => {
  return {
    manifest: {} as Manifest,
    outputCount: 0,
    reset() {
      this.manifest = {}
      this.outputCount = 0
    },
  }
})

perEnvironmentState 是 Vite 的通用工具函数,基于 WeakMap<Environment, State> 实现。它确保在多环境构建(如同时构建 client 和 ssr)时,每个环境拥有独立的 manifest 状态,互不干扰。

14.5.4 多输出合并逻辑

当存在多个 output 配置时(例如 @vitejs/plugin-legacy 同时输出现代和遗留两套产物),Manifest 插件需要合并多次 generateBundle 的结果:

typescript
const state = getState(this)
state.outputCount++
state.manifest = Object.assign(
  state.manifest,
  manifest ?? JSON.parse(asset.source.toString()),
)
if (state.outputCount >= outputLength) {
  // 最后一次输出,写入完整的 manifest
  asset.source = JSON.stringify(state.manifest, undefined, 2)
  state.reset()
} else {
  // 中间输出,暂时删除 manifest 文件
  delete bundle[outPath]
}

这种"收集 -> 合并 -> 最终写入"的模式避免了中间状态的 manifest 文件被输出到磁盘。

14.6 Module Preload

14.6.1 问题与方案

传统的动态 import() 存在一个性能问题:浏览器只有在解析并执行到 import() 语句时,才会开始请求异步 chunk。而这个异步 chunk 可能还有自己的静态依赖,形成请求瀑布流(waterfall)。

Module Preload 的解决方案是:在执行 import() 之前,通过 <link rel="modulepreload"> 或自定义预加载逻辑,并行预加载该 chunk 及其所有静态依赖和关联 CSS。

14.6.2 预加载辅助函数

Vite 在 importAnalysisBuild.ts 中定义了 __vitePreload 辅助函数。在构建时,每个动态 import() 都被包裹为对 __vitePreload 的调用:

javascript
// 构建前
const module = await import('./AsyncComponent.vue')

// 构建后
const module = await __vitePreload(
  () => import('./AsyncComponent-abc123.js'),
  ['./assets/dep-xyz789.js', './assets/style-def456.css']
)

__vitePreload 的核心实现:

typescript
function preload(
  baseModule: () => Promise<unknown>,
  deps?: string[],
  importerUrl?: string,
) {
  let promise = Promise.resolve()
  if (true && deps && deps.length > 0) {
    const links = document.getElementsByTagName('link')
    const cspNonce = document.querySelector('meta[property=csp-nonce]')
      ?.nonce || /* fallback */ getAttribute('nonce')

    promise = allSettled(
      deps.map((dep) => {
        dep = assetsURL(dep, importerUrl)
        if (dep in seen) return  // 去重
        seen[dep] = true

        // 检查是否已被 SSR 预加载
        if (isBaseRelative) {
          for (let i = links.length - 1; i >= 0; i--) {
            if (links[i].href === dep) return
          }
        }

        // 创建预加载 <link>
        const link = document.createElement('link')
        link.rel = isCss ? 'stylesheet' : scriptRel
        link.crossOrigin = ''
        link.href = dep
        if (cspNonce) link.setAttribute('nonce', cspNonce)
        document.head.appendChild(link)

        if (isCss) {
          return new Promise((res, rej) => {
            link.addEventListener('load', res)
            link.addEventListener('error', () =>
              rej(new Error(`Unable to preload CSS for ${dep}`)))
          })
        }
      }),
    )
  }
  return promise.then((res) => {
    for (const item of res || []) {
      if (item.status !== 'rejected') continue
      handlePreloadError(item.reason)
    }
    return baseModule().catch(handlePreloadError)
  })
}

关键设计点:

  1. true 条件编译:在非现代浏览器的 legacy chunk 中,预加载逻辑被完全移除
  2. seen 去重表:全局单例对象,避免同一资源被重复预加载
  3. SSR 标记检测:反向遍历已有的 <link> 标签,避免与 SSR 渲染的预加载标签重复
  4. CSP Nonce 支持:从 <meta property="csp-nonce"> 获取 nonce 值并注入到动态创建的 <link> 标签
  5. allSettled 语义:使用 Promise.allSettled 确保即使某个依赖预加载失败,其他依赖和主模块仍能正常加载
  6. vite:preloadError 事件:通过自定义 DOM 事件允许应用层处理预加载失败,支持 preventDefault 阻止错误冒泡

14.6.3 Polyfill 注入

modulePreloadPolyfill.ts 负责在不支持 modulepreload 的浏览器中注入 Polyfill:

typescript
export function modulePreloadPolyfillPlugin(config: ResolvedConfig): Plugin {
  if (config.isBundled) {
    return perEnvironmentPlugin(
      'native:modulepreload-polyfill',
      (environment) => {
        return nativeModulePreloadPolyfillPlugin({
          isServer: environment.config.consumer !== 'client',
        })
      },
    )
  }
  // 开发模式下返回空模块
  return {
    name: 'vite:modulepreload-polyfill',
    load: {
      filter: { id: exactRegex(resolvedModulePreloadPolyfillId) },
      handler(_id) {
        return ''  // 开发模式不需要 polyfill
      },
    },
  }
}

该插件通过 perEnvironmentPlugin 实现环境感知:在客户端环境中注入 Polyfill(用于检测浏览器是否支持 <link rel="modulepreload">),在服务端环境中完全跳过。开发模式下返回空模块,因为开发服务器通过 ESM 原生加载,不需要预加载机制。

14.6.4 预加载代码生成

getPreloadCode 函数根据环境配置生成不同的预加载辅助代码:

typescript
function getPreloadCode(environment, renderBuiltUrlBoolean, isRelativeBase) {
  const { modulePreload } = environment.config.build

  // 决定 <link rel="?"> 的值
  const scriptRel =
    modulePreload && modulePreload.polyfill
      ? `'modulepreload'`  // 有 polyfill 保证,直接使用
      : `(${detectScriptRel.toString()})()`  // 运行时检测

  // 资源 URL 解析函数
  const assetsURL =
    renderBuiltUrlBoolean || isRelativeBase
      ? `function(dep, importerUrl) { return new URL(dep, importerUrl).href }`
      : `function(dep) { return ${JSON.stringify(base)}+dep }`

  return `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}`
}

14.7 设计决策分析

14.7.1 可选依赖与惰性加载

Terser 在 Vite v3 之后被设为可选依赖。这一决策的动机是多方面的:

typescript
function loadTerserPath(root: string) {
  if (terserPath) return terserPath  // 缓存

  const resolved =
    nodeResolveWithVite('terser', undefined, { root }) ??
    nodeResolveWithVite('terser', _dirname, { root })
  if (resolved) return (terserPath = resolved)

  throw new Error(
    'terser not found. Since Vite v3, terser has become an optional dependency. You need to install it.',
  )
}
  • 安装速度:减少默认安装的依赖数量,Terser 本身约 3MB
  • 灵活性:大多数项目使用 esbuild 或 oxc 压缩即可,不需要 Terser
  • 错误友好:当用户配置 minify: 'terser' 但未安装时,提供清晰的错误提示

14.7.2 Native 与兼容层的分层策略

Manifest 插件的三层架构展示了 Vite 从 Rollup 向 Rolldown 迁移的过渡策略:

这种分层架构使得 Rolldown 可以逐步内化 Vite 的扩展逻辑,最终达到"一次调用,全部完成"的目标。

14.7.3 产物优化的渐进式管线

整个产物优化过程形成一条精确编排的管线:

14.8 小结

本章深入分析了 Vite 产物优化的六大核心机制:

  • 代码分割利用 Rolldown 的模块图分析实现入口分割、动态分割和公共模块提取,CSS 代码分割与 JS chunk 保持关联以支持并行加载。preserveEntrySignaturesmanualChunks 提供了灵活的控制手段。
  • Tree Shaking 基于 ESM 的静态语义实现死代码消除,通过模块级 sideEffects 和表达式级 /*#__PURE__*/ 两级标记体系提供精确的消除依据。Vite 在库模式下特别保留 pure annotation 以支持下游的二次 Tree Shaking。
  • Terser 压缩采用 WorkerWithFallback 实现多线程并行压缩,通过 shouldUseFake 检测优雅处理不可序列化配置的主线程回退。惰性加载策略确保了 Terser 作为可选依赖的零成本。
  • License 提取通过遍历 chunk 的 moduleIds,自动从 node_modules 中提取许可证信息,使用 name@version 去重,支持 Markdown 和 JSON 两种输出格式。
  • Manifest 生成采用三层插件协作架构,在 Rolldown native 实现和 Vite 兼容层之间实现平滑过渡,perEnvironmentState 确保了多环境构建的状态隔离。
  • Module Preload 通过构建时分析依赖图并注入 __vitePreload 辅助函数,将动态导入的瀑布流请求转化为并行预加载。去重、CSP Nonce、SSR 检测、错误事件等细节设计展示了生产级代码的严谨性。

这些优化手段在构建管线中按照精确的顺序协同执行,共同将开发者的源代码转化为高性能的生产产物。

基于 VitePress 构建