首页
/ Create React App 生产构建解析:build 目录产物、Contenthash 长期缓存与 --profile 性能剖析

Create React App 生产构建解析:build 目录产物、Contenthash 长期缓存与 --profile 性能剖析

2026-09-04 10:52:16作者:史锋燃Gardner

本篇基于 create-react-app 仓库中的官方文档与 react-scripts 源码,讲透 npm run build 生产构建的完整工作机制:build/ 目录中各类 JS chunk 的由来、文件名中 contenthash 如何支撑长期缓存策略、INLINE_RUNTIME_CHUNK 环境变量如何控制 runtime 内联,以及 --profile 标志如何在生产构建中启用 React Profiling。读完你可以直接复制可用的 Cache-Control 配置,并理解 CRA 生产构建从源码层面的每个关键决策。

npm run build 究竟做了什么

执行 npm run build 会在项目根目录生成一个 build 目录,其中包含应用的生产构建产物;JavaScript 和 CSS 文件位于 build/static 目录下,且每个文件名中都内嵌了基于文件内容计算出的唯一哈希。正是这个文件名哈希,支撑了后文所述的长期缓存(Long Term Caching)策略。

从源码看,构建入口是 build.js。它在脚本一开始就做了两件关键的事:

  1. BABEL_ENVNODE_ENV 都设置为 production,让 Babel 加载生产预设(create.js 会据此选择 prod 预设,并从生产构建中移除 PropTypes),同时让 webpack.config.js 走生产分支;
  2. 调用 configFactory('production') 生成 webpack 配置后,依次执行:读取 build 目录现有文件体积 → 清空目录并复制 public/ 文件夹 → 启动 webpack 构建 → 打印 gzip 后的体积报告与部署提示。

构建脚本中还有几个值得注意的源码级行为:

  • 体积告警阈值:gzip 后总 bundle 超过 512 KB、单个 chunk 超过 1 MB 时会打印警告(WARN_AFTER_BUNDLE_GZIP_SIZE / WARN_AFTER_CHUNK_GZIP_SIZE,见 build.js);
  • CI 严格模式:当 process.env.CI 为真时,warning 会被提升为 error 并使构建失败(源码中特意过滤了 Failed to parse source map 类警告);
  • --stats 标志npm run build -- --stats 会将 webpack 的完整 stats 写入 build/bundle-stats.json,配合 bundle 分析工具排查包体积;
  • public/ 文件夹合并copyPublicFolder() 会把 public/ 下除 index.html 外的所有文件原样拷贝进 build/,因此静态资源(如 favicon.icorobots.txt)会直接出现在产物中。

解读 build/static/js 中的三类 chunk

对一个刚创建、未做任何代码拆分的 CRA 应用执行生产构建后,build/static/js 中会生成若干 .js 文件(webpack 称之为 chunk)。根据 output 配置,主 chunk 与异步 chunk 的文件名模板分别为 static/js/[name].[contenthash:8].jsstatic/js/[name].[contenthash:8].chunk.js。具体产物分三类:

main.[hash].chunk.js —— 应用代码

这是你自己的应用代码,即 App.jsindex.jssrc/ 下的模块打包后的结果。

[number].[hash].chunk.js —— vendor 代码或代码分割 chunk

这类以数字命名的文件有两种来源:

  • vendor 代码:你从 node_modules 中导入的第三方模块会被单独拆出;
  • 代码分割 chunk:如果你使用了 React.lazy / 动态 import()代码分割,每次分割都会在 build/static 下额外生成对应的 chunk 文件。

将 vendor 与 application 代码分离的核心价值在于长期缓存:vendor 代码(第三方库)通常比应用代码变更得更少,浏览器可以分别缓存它们,应用代码更新时无需重新下载 vendor 部分,从而改善首屏加载性能。

runtime-main.[hash].js —— webpack runtime

这是一个体积很小的 webpack runtime 逻辑块,负责加载并运行你的应用。默认情况下,它的内容会被内联嵌入到 build/index.html 中,以省掉一次额外的网络请求。通过设置 INLINE_RUNTIME_CHUNK=false 可以关闭这一行为,让 runtime 作为独立 chunk 加载——这一点在 高级配置文档 中有对应说明。

INLINE_RUNTIME_CHUNK 的源码实现

这个开关在 webpack.config.js 中定义:

const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';

注意其默认值是 true,只有显式设置为字符串 'false' 才关闭内联。内联动作由 InlineChunkHtmlPluginwebpack.config.js 中完成,它通过 HtmlWebpackPlugin 的 alterAssetTagGroups 钩子,把匹配 /runtime-.+[.]js/ 的正则的 chunk 内容从 <script src> 标签改写为内联 <script> 内容:

shouldInlineRuntimeChunk &&
  new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),

典型使用场景是受 CSP(内容安全策略)限制的环境,服务器不允许 HTML 中出现内联脚本,此时将 INLINE_RUNTIME_CHUNK 设为 false,runtime 会以普通外部 chunk 的形式被加载。

静态文件缓存:用文件名哈希实现长期缓存

build/static 中的每个文件名都会追加一个由文件内容生成的唯一哈希(webpack 的 contenthash,见上文 static/js/[name].[contenthash:8].js 模板)。内容不变则哈希不变,内容变化则文件名随之改变,这让 CDN 和浏览器可以放心地对静态资源做激进缓存——只要文件内容没变,浏览器就无需重新下载;一旦内容变化,新文件名会天然绕开旧缓存。

最佳实践是为 index.htmlbuild/static 下的文件分别配置 Cache-Control 响应头,由你控制的 CDN/静态服务器决定缓存时长。一个安全且高效的起点是:

  • build/static 下的资源:Cache-Control: max-age=31536000(缓存一年);
  • 对其余所有文件(尤其是 index.html):Cache-Control: no-cache(每次协商校验)。

这套组合保证了用户浏览器每次都会重新校验 index.html(其中引用了带最新哈希的资源路径),同时静态资源在一年内不会被重复请求。之所以可以安全地对 build/static 使用一年过期时间,正是因为文件名中内嵌了内容哈希——内容一变,URL 就变了,缓存永不"过期失效"。

生产构建中的 Profiling:--profile 标志

React 16.5+ 在开发模式下自动支持 Profiling,但由于 Profiling 会带来少量额外开销,生产模式下默认关闭,属于显式开启(opt-in)。开启方式是给构建命令追加 --profile 标志:

# 使用 npm
npm run build -- --profile

# 使用 yarn
yarn build --profile

从源码看,这个标志触发了两处与"保留可调试名称、指向 profiling 构建"直接相关的配置。

第一处是 webpack.config.js 中的标志检测,以及据此生效的模块别名替换:

// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
  isEnvProduction && process.argv.includes('--profile');
// ...
alias: {
  // Allows for better profiling with ReactDevTools
  ...(isEnvProductionProfile && {
    'react-dom$': 'react-dom/profiling',
    'scheduler/tracing': 'scheduler/tracing-profiling',
  }),
},

开启后,react-dom 会被替换为 react-dom/profiling 入口、scheduler/tracing 指向 profiling 版本,这是 React DevTools 在构建产物中识别组件、做 Performance 追踪的前提。

第二处是 Terser 压缩配置(webpack.config.js)中根据同一标志保留类名与函数名:

// Added for profiling in devtools
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,

因为 Profiling 依赖组件的类名/函数名来标识节点,若不保留,压缩后的匿名标识会让 DevTools 的剖析结果几乎不可读。构建完成后,即可在 React DevTools 的 Profiler 标签中录制并分析生产构建中各组件的渲染耗时。

小结

关注点 机制 关键源码位置
产物文件名哈希 static/js/[name].[contenthash:8].js 等模板 webpack.config.js
runtime 内联开关 INLINE_RUNTIME_CHUNK !== 'false' webpack.config.jsInlineChunkHtmlPlugin
缓存建议 max-age=31536000 + no-cache 组合 production-build 文档
生产 Profiling --profilereact-dom/profiling 别名 + 保留函数/类名 webpack.config.js
体积告警阈值 gzip 后 512 KB / 1 MB build.js

以上均基于当前仓库中 react-scripts 5.1.0 的实现;其中 --profile 行为要求项目使用 React 16.5+。完整的构建后部署与静态托管细节可参考仓库文档 deploymentadvanced-configuration

登录后查看全文
热门项目推荐
相关项目推荐