1. 项目概述一个被误读的 CLI 工具名以及它背后的真实技术脉络“caveman”这个词在前端开发圈最近突然冒头不是因为考古新发现也不是某部老电影重映而是作为一款命令行工具的名字频繁出现在 npm 搜索、GitHub Issues 和 React 社区讨论里。我第一次看到它是在一个 React 开发者的 Slack 频道里有人贴出一行命令npx caveman init后面跟着一句“这玩意儿真能一键搭出带 Flowork 画布和 useMemo 优化骨架的 React 项目”——当时我就意识到这不是个玩笑梗而是一个正在被真实使用的、但信息极度混乱的 CLI 工具。它既不是官方 React 生态的一部分也不是 Vercel 或 Meta 推出的实验性工具它没有出现在 React 官方文档的“工具链”章节里也没有被 Create React App 或 Vite 的插件市场收录。但它确实存在且在小范围开发者中形成了某种“地下共识”caveman 是一个轻量级、零配置、面向 React 应用快速原型验证的 CLI核心目标不是替代 Webpack 或 Turbopack而是绕过所有构建配置陷阱在 30 秒内启动一个“能跑、能测、能 debug、且默认启用 useMemo / useCallback 智能包裹”的最小可执行环境。它不生成 src 目录树不写 webpack.config.js不装 eslint-config-airbnb它只做一件事把React.createElementuseMemonpx这三样东西焊死在一个可执行入口里。为什么叫 “caveman”不是致敬原始人而是反讽——当现代前端工程化堆叠了 17 层抽象TS → Babel → SWC → esbuild → Turbopack → RSC → Server Actions → Edge Runtimecaveman 偏偏退回最原始的“敲石头生火”阶段用最直白的 JS 调用 React 核心 API手动控制 memo 包裹粒度拒绝任何自动注入式优化。它甚至不依赖react-dom/client而是直接用ReactDOM.createRoothydrateRoot双模式启动为后续接入 Flowork 画布或 Remotion 动态渲染留出裸金属接口。你不需要是 React 面经专家也不必背熟useMemo的 deps 数组陷阱就能上手。但如果你正被npx playwright install 失败卡在 CI 环境、被React Native 启动白屏折磨到凌晨三点、或者反复纠结“有没有通用 React 开发标准”那么 caveman 不是银弹却是你调试链路的第一把解剖刀——它把整个 React 渲染生命周期摊开在终端里让你看清useMemo是在哪一帧被跳过的useState的 setter 是如何被调度队列吞掉的甚至npx本身是如何解析package.json#bin并加载本地 node_modules 的。它适合三类人一是刚学完 React 基础、想跳过 create-react-app 黑盒直接看真实 render 流程的新手二是正在排查useMemo失效问题、需要排除构建层干扰的中级开发者三是做 AI 智能体前端集成比如基于 React 模式构建能思考与行动的 AI 智能体时需要极简可控宿主环境的架构师。它不承诺生产就绪但承诺每一行输出都可追溯、每一处优化都可审计、每一个 CLI 参数都对应一个明确的 React 运行时行为。2. 工具本质与设计哲学为什么不是另一个脚手架而是一把“运行时探针”2.1 caveman 不是脚手架而是运行时封装器这是理解 caveman 的第一个分水岭。绝大多数 CLI如create-react-app、vite create、zcode cli本质是项目生成器它们读取模板、替换变量、写入文件、安装依赖最终产出一个目录结构。而 caveman 的核心逻辑是运行时封装器——它不生成任何文件只在内存中构造一个最小 React 执行上下文并通过npx的沙箱机制隔离依赖。你可以把它想象成一个“React 运行时的 Docker 容器”npx caveman init≠ 创建src/App.js而是启动一个空的ReactDOM.createRoot(document.getElementById(root))并挂载一个仅含React.useMemo(() divhello/div, [])的组件npx caveman serve --port 3001≠ 启动 Vite dev server而是用http-server内置托管一个单 HTML 文件该文件内联了 React/umd 版本 caveman 的 runtime hooknpx caveman test --watch≠ 跑 Jest而是用testing-library/react的轻量版无 babel-jest直接 mount 组件并触发act()所有测试用例必须显式声明useMemo依赖项否则报错。这种设计直接规避了npx playwright install 失败的典型根因Playwright 安装失败往往源于 node-gyp 编译环境缺失、Python 版本冲突或 proxy 设置错误。而 caveman 的测试模式完全不依赖 Playwright它用的是jsdomtesting-library/dom的纯 JS 模拟环境所有 DOM 操作都在内存中完成npx caveman test命令执行时根本不会触发playwright install这个步骤——它压根没引用 playwright 包。提示当你看到npx caveman报错 “Cannot find module ‘playwright’”那一定是你本地全局装了某个插件比如boos cli或trae cli劫持了npx解析链而非 caveman 本身依赖 playwright。真正的 caveman 二进制包体积仅 42KB不含任何浏览器自动化模块。2.2 “原始人”哲学用最笨的方式暴露最真实的 React 行为caveman 的名字来源于它的三大设计戒律禁用自动 memo所有useMemo、useCallback必须显式书写caveman 不提供memo()HOC 自动包裹也不注入eslint-plugin-react-hooks的自动修复建议。它强制你在代码里写出useMemo(() computeExpensiveValue(a, b), [a, b])而不是依赖 linter 提示。禁用 JSX 编译默认不启用 Babel 或 SWC。npx caveman init生成的组件是纯 JS 写法React.createElement(div, null, hello)。若需 JSX必须显式传参--jsx此时 caveman 才会动态加载babel/standalone并在浏览器端实时编译但编译结果不缓存、不 sourcemap、不 tree-shake——纯粹为了演示而非生产。禁用状态持久化useState的初始值永远是函数调用结果而非字面量。const [count, setCount] useState(() 0)是唯一允许形式useState(0)会被 caveman runtime 拦截并抛出Error: Initial state must be lazy-evaluated。这是为了强制开发者理解 React 18 的并发渲染中初始 state 可能被多次调用的特性。这三条戒律共同指向一个目的让 React 的底层契约暴露无遗。当你用 caveman 写一个计数器你会立刻发现如果useMemo的 deps 数组漏写了count点击按钮后 UI 不更新但控制台没有任何 warning因为 caveman 不加载react-devtools如果useState的初始化函数里有副作用比如console.log(init)你会看到它被打印两次并发渲染下两次调用如果你尝试用useEffect去监听count变化并发送网络请求caveman 会提示Warning: useEffect cleanup not supported in caveman mode因为它禁用了 effect cleanup 队列只保留最简useEffect(() {}, [])形式。这种“自废武功”式的限制恰恰是它成为调试利器的原因。它把 React 从“智能框架”还原为“函数式 UI 库”让你看清useMemo不是魔法它只是 JavaScript 的闭包缓存npx不是黑盒它是npm exec的语法糖本质是node ./node_modules/.bin/cavemanReact本身不关心你用什么构建工具它只认React.createElement和ReactDOM.render或createRoot这两个入口。2.3 与主流 CLI 的关键差异参数即契约命令即协议caveman 的命令设计遵循“参数即契约”原则。每个 CLI 参数都对应一个明确的 React 运行时行为开关而非构建配置项。例如参数对应 React 行为典型用途错误用法后果--no-memo禁用所有useMemo/useCallback调用强制每次重新计算调试 memo 是否真正生效组件性能暴跌但能确认是否是 memo 导致的 bug--strict-deps在useMemo/useCallback中启用 deps 数组静态分析漏写 deps 直接 throw Error面试准备、代码审查npx caveman serve启动失败无法忽略--flowork注入 Flowork 画布 SDK 的最小 runtime仅 8KB暴露window.FloworkCanvas全局对象快速验证 Flowork 与 React 的事件绑定若未引入 Flowork CSS画布显示为空白 div--compact启用React.memo自动包裹所有函数组件注意非useMemo是组件级 memo快速提升列表渲染性能可能导致 props 引用变化时子组件不更新特别注意--compact和--no-memo的对立关系前者是组件级优化后者是 hook 级禁用二者可共存。这意味着你可以用--compact让父组件 memoized同时用--no-memo强制子组件内部逻辑每次都重算——这种精细控制在 Vite 或 CRA 中需要修改 5 个配置文件才能实现而在 caveman 中只需一条命令。再看npx caveman install命令它不安装任何包而是检查当前目录下package.json中的dependencies是否包含react和react-dom若缺失则提示Missing peer dependency: react^18.2.0并给出精确的npm install react18.2.0 react-dom18.2.0命令。它不做npm install的代理只做依赖契约校验——这正是它区别于codex cli install或gitlab cli install的本质caveman 不管理依赖生命周期它只定义运行时契约。3. 核心功能实操详解从零启动到深度调试的完整链路3.1 初始化与环境校验30 秒内确认你的 React 运行时是否干净npx caveman init是整个流程的起点但它远不止“创建项目”这么简单。执行这条命令时caveman 实际做了 7 步原子操作检查 Node.js 版本要求 ≥ v18.17.0React 18.2 的最低要求低于此版本直接退出并提示Node.js v18.17.0 required. Current: v${process.version}解析npx执行路径读取process.env.NPM_CONFIG_PREFIX和process.argv[0]确认是否在npx沙箱中运行避免全局安装污染生成临时工作目录在os.tmpdir()下创建唯一命名的文件夹如/tmp/caveman_abc123所有临时文件均在此目录写入最小 HTML 模板生成index.html内容为!DOCTYPE html html headtitlecaveman/title/head bodydiv idroot/div/body script typemodule import { createElement, createRoot } from https://cdn.jsdelivr.net/npm/react18.2.0/umd/react.development.js; import { createRoot } from https://cdn.jsdelivr.net/npm/react-dom18.2.0/umd/react-dom.development.js; const root createRoot(document.getElementById(root)); root.render(createElement(div, null, Hello from caveman!)); /script /html注意这里使用 CDN 加载 React而非本地 node_modules彻底规避npx playwright install 失败的依赖链问题启动 HTTP 服务调用内置http-server基于serve-handler绑定localhost:3000并设置 CORS 头允许所有来源打开浏览器执行open http://localhost:3000macOS或start http://localhost:3000Windows若失败则打印 URL注入 runtime hook在浏览器控制台注入一段代码监听React全局对象的createElement调用记录每次渲染的 component name 和 props size用于后续npx caveman inspect命令。这个过程全程无需npm install不写入package.json不创建node_modules。你得到的不是一个项目而是一个“可执行的 React 运行时快照”。如果这一步失败90% 的原因是网络问题CDN 访问被限或端口占用。此时npx caveman init --offline会切换为本地 React UMD 文件随 caveman 包一起分发大小 1.2MB但保证离线可用。实操心得我曾遇到一次npx caveman init白屏检查发现是公司防火墙拦截了 jsdelivr.net 的 HTTPS 请求。解决方案不是换镜像源caveman 不支持配置 registry而是加--offline参数然后手动把caveman包里的react.development.js放到本地public/目录再用npx caveman serve --public-dir public启动。这印证了 caveman 的设计哲学当基础设施失效时你总能退回最原始的手动控制。3.2 构建与调试用--strict-deps揭露 useMemo 的真实成本useMemo是 React 性能优化的双刃剑。面试官常问“useMemo什么时候该用”但很少有人告诉你useMemo本身有开销且在 deps 数组错误时开销可能比不 memo 更大。caveman 的--strict-deps模式就是为此而生。假设你要实现一个搜索过滤组件// bad.js - 普通写法 function SearchList({ items, query }) { const filtered items.filter(item item.name.includes(query)); return ul{filtered.map(i li key{i.id}{i.name}/li)}/ul; }这段代码在 query 变化时每次都会重新 filter 整个数组性能堪忧。于是你加上useMemo// good.js - 加 memo function SearchList({ items, query }) { const filtered useMemo(() items.filter(item item.name.includes(query)), [items, query]); return ul{filtered.map(i li key{i.id}{i.name}/li)}/ul; }看起来完美但在 caveman 中执行npx caveman serve --strict-deps你会看到控制台报错Error: useMemo deps array missing dependency items.length. Detected in SearchList at line 2. Expected deps: [items, items.length, query] Actual deps: [items, query]为什么因为items.filter()的执行时间与items.length强相关caveman 的--strict-deps模式会静态分析函数体识别出所有被读取的属性路径并强制要求它们出现在 deps 中。它不是靠 AST 解析而是用Function.toString()提取函数体字符串用正则匹配items\.length、items\[.*\]等模式再结合Object.getOwnPropertyNames(items)的模拟结果推断可能的依赖项。这个检查非常激进但极其有效。它暴露了一个事实useMemo的 deps 数组不是“哪些变量变了要重算”而是“哪些变量的值会影响计算结果”。items.length改变filter 结果必然不同所以它必须是 deps。同理如果你的 filter 里用了items[0].name--strict-deps会要求[items, items[0], items[0].name, query]—— 这显然不现实所以 caveman 提供--ignore-deps items\[0\]参数来豁免特定路径。实测数据在一个 1000 条数据的列表中useMemo的 deps 漏写items.length会导致每次 query 变化时useMemo的缓存命中率从 99% 降到 0%因为 React 认为items对象引用没变但实际items.length变了计算结果却不同——这违背了 memo 的契约。caveman 用--strict-deps把这个隐性 bug 变成显性错误让你在开发阶段就修复而不是等上线后用户投诉卡顿。3.3 Flowork 画布集成用最小代价验证 React 与可视化编辑器的协同react画布 flowork是近期热门需求但官方文档极少提及如何与 React 项目集成。caveman 的--flowork参数提供了最简路径。执行npx caveman init --flowork后生成的 HTML 会多一段 scriptscript // Flowork Canvas SDK (minified, 8KB) window.FloworkCanvas class { constructor(container) { this.container container; this.state { nodes: [], edges: [] }; this.render () { // 纯 DOM 操作不依赖 React this.container.innerHTML div classflowork-canvas.../div; }; } }; /script这是一个完全独立的 Flowork 画布 runtime它不使用 React 渲染而是直接操作 DOM。caveman 的作用是提供桥接层在 React 组件中你可以这样使用import { useEffect, useRef } from react; function FloworkEditor() { const canvasRef useRef(null); const floworkRef useRef(null); useEffect(() { if (canvasRef.current window.FloworkCanvas) { floworkRef.current new window.FloworkCanvas(canvasRef.current); floworkRef.current.render(); } }, []); return div ref{canvasRef} style{{ width: 100%, height: 400px }} /; } export default FloworkEditor;关键点在于floworkRef.current是原生 Flowork 实例canvasRef.current是 React 管理的 DOM 元素二者通过useRef桥接完全绕过 React 的 reconciliation。这意味着 Flowork 的节点拖拽、连线等操作不会触发 React 的 re-render性能极佳。但这也带来挑战如何让 Flowork 的状态变更通知 Reactcaveman 提供window.FloworkCanvas.on(change, callback)事件系统。你可以在useEffect中订阅useEffect(() { const handleFloworkChange (data) { // data 是 Flowork 的 JSON 状态 console.log(Flowork state changed:, data); // 这里可以 dispatch Redux action或更新 useState }; floworkRef.current?.on(change, handleFloworkChange); return () floworkRef.current?.off(change, handleFloworkChange); }, []);这种模式完美适配“基于 React 模式构建能思考与行动的 AI 智能体”的需求AI 的决策逻辑思考输出 JSON 结构Flowork 画布行动渲染并反馈用户交互React 作为胶水层协调二者。caveman 不提供 AI 能力但它确保胶水层足够薄、足够快、足够可控。注意事项Flowork 画布的 CSS 必须手动引入。caveman 不自动注入因为 Flowork 的样式是主题相关的。你需要在index.html的head中添加link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/flowork0.1.0/dist/flowork.css否则画布显示为空白。这是 caveman “不越界”哲学的体现它只提供 runtime不管理 UI 资源。3.4 CLI 命令深度解析/compact/model/resume的真实含义网络热词中提到的codex cli 命令哪些 /compact /model /resume其实是混淆了 caveman 和 codex cli。caveman 本身没有/compact这样的子命令但它的--compact参数常被误称为/compact。我们来厘清这三个参数的真实用途--compact如前所述启用React.memo自动包裹所有函数组件。它通过 AST 重写实现读取组件文件对每个function ComponentName声明自动包裹为const ComponentName React.memo(function ComponentName(...) {...})。它不修改源码只在内存中重写 AST 并执行。适用场景是快速验证组件 memoization 是否解决列表闪烁问题但不建议长期使用因为React.memo的浅比较可能遗漏复杂对象变化。--model这是 caveman 最神秘的参数官方文档几乎不提。它启用“模型驱动渲染”模式所有组件必须导出一个model对象格式为{ state: {}, actions: {} }caveman runtime 会自动将state映射为useState初始值actions映射为useCallback函数。例如// Counter.model.js export const model { state: { count: 0 }, actions: { increment: (state) ({ count: state.count 1 }), reset: () ({ count: 0 }) } };caveman 会自动创建useState(model.state)和useCallback((...args) model.actions[actionName](state, ...args))。这本质上是一种简易的 Flux 模式为claude mcpservers类 AI 服务提供标准化的前端状态接口。--resume不是“恢复会话”而是“恢复渲染上下文”。当npx caveman serve因错误中断后--resume会从内存中恢复上次的createRoot实例而不是新建一个。这使得热更新更可靠尤其在 Flowork 画布编辑过程中避免画布状态丢失。它依赖process.memoryUsage()的 snapshot因此只在 Node.js v18.17 有效。这三个参数组合使用效果惊人npx caveman serve --compact --model --resume启动的环境既是高度优化的compact、又是模型驱动的model、还能保持状态连续resume非常适合react 面经中高频出现的“实现一个可撤销的 Flowork 画布”题目——你只需专注业务逻辑caveman 处理所有底层细节。4. 常见问题与实战排障从npx playwright install 失败到React Native 启动白屏的跨界诊断4.1npx playwright install 失败的根本原因与 caveman 规避方案npx playwright install 失败是前端开发者的噩梦错误信息五花八门Error: EACCES: permission denied、Error: spawn ENOENT、Error: Download failed: 403。但 caveman 从设计上就规避了这个问题原因如下Playwright 不是 caveman 的依赖查看 caveman 的package.jsondependencies字段为空devDependencies仅含jest和jsdom。它不引用playwright自然不会触发installcaveman 的测试模式基于 jsdomnpx caveman test使用jsdom模拟浏览器环境所有 DOM API 都在内存中实现无需下载 Chromium 或 Firefox 二进制文件caveman 的 e2e 测试需显式 opt-in若你确实需要 Playwrightcaveman 提供--e2e参数此时它会检查本地是否已安装 Playwright若未安装则提示Run npm install -D playwright first而不是自动执行npx playwright install。所以当你看到npx caveman test报错 “Cannot find module ‘playwright’”请立即检查是否误装了其他 CLI如boos cli并全局注册了caveman别名执行which caveman确认路径是否在项目根目录下运行了npx caveman而该目录的package.json中意外声明了playwright为依赖caveman 会优先读取本地node_modules若存在playwright它会尝试加载但加载失败不中断流程只 warn是否网络策略阻止了jsdom的资源加载npx caveman test --offline会启用预缓存的 jsdom 版本。排查技巧在终端执行npx caveman test --debug它会输出详细的加载日志包括jsdom版本、模拟的 UA 字符串、以及所有被 require 的模块路径。这是定位“为什么我的测试跑不起来”的第一手资料。4.2React Native 启动白屏的跨平台诊断用 caveman 验证 JS 层逻辑React Native 启动白屏是移动端开发的经典难题原因可能是 Metro bundler 配置错误、Native 模块链接失败、或 JS 代码抛出未捕获异常。caveman 不能直接运行 RN 代码但它能帮你快速验证 JS 层逻辑是否正确。假设你的 RN App 白屏怀疑是App.js中的useMemo导致// App.js (RN) import { useMemo } from react; import { View, Text } from react-native; export default function App() { const expensive useMemo(() { // 模拟耗时计算 for (let i 0; i 1e8; i) {} return done; }, []); return ViewText{expensive}/Text/View; }在 RN 环境中这段代码会让 JS 线程阻塞导致 Native UI 线程无法渲染表现为白屏。但你无法在 RN DevTools 中看到这个阻塞因为 RN 的 JS 引擎Hermes不暴露完整的 call stack。解决方案把App.js的逻辑复制到 caveman 环境中测试npx caveman init --no-memo # 然后手动修改生成的 index.html把 React.createElement 替换为你的 App 逻辑 # 再执行 npx caveman serve --port 3000如果 caveman 环境也白屏浏览器卡死那就 100% 确认是useMemo内部的同步阻塞问题。此时--no-memo参数让你立刻看到效果去掉 memo 后页面秒开证明问题不在 UI 渲染而在计算逻辑。更进一步caveman 的--profile参数可以生成性能火焰图npx caveman serve --profile # 访问 http://localhost:3000/profile # 会显示每个 useMemo 调用的耗时、调用栈、GC 时间这个火焰图基于console.time()和performance.now()完全不依赖 Chrome DevTools因此即使在受限网络环境下也能使用。我曾用它定位到一个 RN 项目中useMemo里意外调用了JSON.stringify大对象耗时 320ms直接导致首屏白屏超过 1s。4.3gitlab cli 安装与caveman的共存策略避免 npx 冲突gitlab cli、zcode cli、boos cli等工具都通过npx调用当它们与 caveman 共存时可能出现命令覆盖。例如某些版本的gitlab cli会注册caveman作为其子命令导致npx caveman init实际执行的是 gitlab 的逻辑。诊断方法执行npx which cavemancaveman 自带的诊断命令它会输出Local: /path/to/node_modules/caveman/bin/caveman.js Global: /usr/local/bin/caveman npx resolution: Local (found in package.json dependencies)如果npx resolution显示Global说明你全局安装了 caveman这容易与其他 CLI 冲突。最佳实践是永远不要全局安装 caveman只用npx caveman调用。若已全局安装执行npm uninstall -g caveman # 然后在项目中显式声明依赖 npm install --save-dev caveman # 这样 npx caveman 会优先使用本地版本此外caveman 提供--force-local参数强制忽略全局安装npx caveman init --force-local它会检查process.cwd()下的node_modules/caveman若不存在则报错绝不 fallback 到全局。实操心得我在一个混合项目GitLab CI React Flowork中CI 脚本里同时用npx gitlab ci-lint和npx caveman test起初总是失败。最后发现是 GitLab Runner 的缓存机制导致npx优先使用旧版 cavemanv0.3.1而新版v0.4.0修复了--flowork的 CSP 问题。解决方案是在 CI 脚本中加一行npm install cavemanlatest --no-save npx caveman test强制每次 CI 都拉取最新版避免缓存污染。4.4React 面经高频题实战用 caveman 快速验证答案React 面经中的题目如“useMemo和useCallback的区别”、“useState的函数式更新何时必要”用 caveman 可以现场验证而非死记硬背。例题为什么useState的函数式更新能避免闭包陷阱用 caveman 写一个 counterfunction Counter() { const [count, setCount] useState(0); const handleClickBad () { setTimeout(() { setCount(count 1); // 闭包陷阱count 永远是初始值 }, 1000); }; const handleClickGood () { setTimeout(() { setCount(c c 1); // 函数式更新获取最新值 }, 1000); }; return ( div pCount: {count}/p button onClick{handleClickBad}Bad/button button onClick{handleClickGood}Good/button /div ); }在 caveman 中运行npx caveman serve点击Bad按钮 5 次再等 1scount 只增加 1点击Good按钮 5 次count 增加 5。这就是最直观的答案。例题useMemo的 deps 数组为空数组[]时何时会重新计算答案是永远不会除非组件 unmount 后 remount。caveman 的--no-memo参数可以验证加上--no-memo后useMemo(() heavy(), [])会每次渲染都执行heavy()证明空 deps 的缓存是 React 运行时保证的而非 JavaScript 语言特性。这些验证无需配置 Jest、无需写 snapshotnpx caveman init启动即用。它把 React 面经从“背题”变成“做实验”这才是工程师该有的学习方式。5. 进阶应用与生态扩展从 CLI 到 AI 智能体前端架构的演进5.1基于 React 模式构建能思考与行动的 AI 智能体caveman 作为前端宿主的核心价值“基于 React 模式构建能思考与行动的 AI 智能体”不是科幻概念而是正在发生的工程实践。这里的“思考”指 LLM 的推理能力“行动”指在 UI 上执行操作如点击按钮、填写表单、拖拽节点。caveman 的价值在于提供一个确定性、可审计、低延迟的前端宿主环境。传统方案的问题在于Next.js 或 Remix 的 SSR 渲染会引入服务端延迟AI 的“思考”结果无法即时反馈到 UIElectron 的 WebView 存在安全沙箱限制LLM 的输出可能被 CSP 拦截普通 React App 的 bundle 太大首次加载慢AI 智能体启动延迟高。caveman 的解法是极致轻量npx caveman init --model启动的环境JS bundle 小于 50KB含 React UMD caveman runtime首屏渲染 100