Create React App 生产构建解析:build 目录产物、Contenthash 长期缓存与 --profile 性能剖析
本篇基于 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。它在脚本一开始就做了两件关键的事:
- 将
BABEL_ENV和NODE_ENV都设置为production,让 Babel 加载生产预设(create.js 会据此选择 prod 预设,并从生产构建中移除 PropTypes),同时让 webpack.config.js 走生产分支; - 调用
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.ico、robots.txt)会直接出现在产物中。
解读 build/static/js 中的三类 chunk
对一个刚创建、未做任何代码拆分的 CRA 应用执行生产构建后,build/static/js 中会生成若干 .js 文件(webpack 称之为 chunk)。根据 output 配置,主 chunk 与异步 chunk 的文件名模板分别为 static/js/[name].[contenthash:8].js 和 static/js/[name].[contenthash:8].chunk.js。具体产物分三类:
main.[hash].chunk.js —— 应用代码
这是你自己的应用代码,即 App.js、index.js 等 src/ 下的模块打包后的结果。
[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' 才关闭内联。内联动作由 InlineChunkHtmlPlugin 在 webpack.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.html 与 build/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.js、InlineChunkHtmlPlugin |
| 缓存建议 | max-age=31536000 + no-cache 组合 |
production-build 文档 |
| 生产 Profiling | --profile → react-dom/profiling 别名 + 保留函数/类名 |
webpack.config.js |
| 体积告警阈值 | gzip 后 512 KB / 1 MB | build.js |
以上均基于当前仓库中 react-scripts 5.1.0 的实现;其中 --profile 行为要求项目使用 React 16.5+。完整的构建后部署与静态托管细节可参考仓库文档 deployment 与 advanced-configuration。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00