caveman:零依赖、零构建的极简静态站点方案实践 我最近把自己一个小落地页项目从头重写了一遍把能拆的依赖全拆掉越用越顺手就给它起了个名字叫caveman。这个名字有两层意思一是整个项目像穴居人一样原始二也是提醒自己很多页面根本不需要被现代前端工具链拖那么重。如果你手上也有个人页面、产品介绍页、博客这类偏静态的项目或者正被 node_modules 的体积和构建时间折腾得难受那这篇文章应该能给你一些新思路。caveman 不是什么框架也不是什么新语言它就是一套极简的静态站点方案核心是原生 HTML CSS JavaScript零构建、零依赖、直接上传就能跑。我会从立项动机一直讲到我实际使用中碰到的坑包括文件组织、组件写法、性能对比、部署流程和一些能加分的原生优化技巧尽量把能“抄作业”的部分都摊开讲。1. 立项原因依赖地狱让我决定做个体面的“原始人”1.1 一个落地页塞下几百兆依赖的荒诞现实事情的起因有点讽刺。当时我接了个公司内部的产品活动页需求就是一张图、一段视频、三个按钮、一个报名表单外加一小段滚动动画。按惯例我用 React Vite 开了个项目顺手装了路由、动画库、组件库结果整个项目 node_modules 装完接近 400MB开发服务器首次冷启动花了将近 20 秒。页面本身其实只有 6KB 的业务代码但为了这 6KB我得维护一套几十个依赖的复杂关系网。后来同事要接手这个活动页光把开发环境跑起来就折腾了一下午。那一刻我就在想这真的合理吗一个浏览几分钟就关掉的页面凭什么要让人先啃一遍 Webpack/Vite 的配置再等几分钟的依赖安装和编译1.2 “穴居人”项目的三条铁律所以我决定用 caveman 这个名字给自己立三条规矩做一个小而“原始”的项目。零运行时依赖页面跑起来之后不加载任何第三方 JS/CSS 库浏览器能原生做到的事绝不用库。零构建步骤源码就是部署产物写完直接扔到静态托管平台上没有打包、没有转译、没有压缩步骤连.map文件都不存在。离线可维护整个项目用文本编辑器打开就能看懂任何一个前端开发者接手都不需要安装任何包管理器。这三条规矩不是装酷而是对应着实际痛点依赖体积、环境搭建成本、长期维护的遗忘成本。很多项目当初用框架是因为要上复杂交互但落地页、博客、文档站这类内容型页面原生三件套完全足够。我把项目做成“原始人”换来的是极致的可控和透明。1.3 谁适合这个方案这里我必须说清楚适用边界避免你拿它硬套所有场景。适合用 caveman 这类方案的人个人博客、作品集、营销落地页、产品介绍页这种以内容展示为主的站点原型设计与快速验证阶段的小页面对性能和首屏加载有极致要求的静态页面希望长期低维护、几个月后才来改一次内容的个人项目不适合的人需要复杂状态管理、多级路由、实时协作的重交互应用团队所有成员都依赖组件库和统一设计体系的场景需要做复杂代码拆分、按需加载的巨型项目一句话总结caveman 适合“页面”而不是“应用”。如果你要做的是一个工具型产品该上框架还是上框架没必要为极简而极简。2. 项目骨架零依赖的原生三件套怎么组织2.1 文件结构设计一个没有构建工具的项目文件结构必须人为地保持清晰否则一样会乱。我的 caveman 项目默认长这样caveman/ ├── index.html ├── pages/ │ ├── about.html │ └── contact.html ├── assets/ │ ├── css/ │ │ ├── base.css │ │ ├── components.css │ │ └── pages.css │ ├── js/ │ │ ├── main.js │ │ └── modules/ │ │ ├── nav.js │ │ ├── form.js │ │ └── scroll.js │ └── images/ └── favicon.svg这个结构的逻辑是HTML 页面按真实访问路径放CSS 按职责拆成基础样式、组件样式、页面专属样式三个文件JS 用modules/目录装功能模块所有静态资源放assets下。没有构建工具时所有路径都是浏览器直接解析的相对路径所以命名和目录层级必须稳定。不用担心文件多了请求会变多现代浏览器支持 HTTP/2同域名下几十个小文件并行下载的成本很低而且这些文件都有本地缓存第二次访问基本秒开。反而是那种把全部代码压成一个几 MB 的单文件首屏解析和执行压力更大。2.2 用现代 CSS 变量和原生嵌套代替预处理器以前大家用 Sass 或 Less主要是为了变量、嵌套、混合宏。现在原生 CSS 已经有--var变量、嵌套语法、calc()、clamp()、container这些能力。我的经历是一个切图仔平均用到的 Sass 功能原生 CSS 能覆盖八成以上。比如定义一套颜色和间距变量:root { --color-bg: #f7f6f2; --color-text: #2b2b2b; --color-primary: #6b4f2a; --space-sm: 0.5rem; --space-md: 1rem; --space-lg: 2rem; --font-sans: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }页面里.card { background: var(--color-bg); padding: var(--space-lg); font-family: var(--font-sans); border: 1px solid color-mix(in srgb, var(--color-primary) 20%, transparent); }color-mix()、clamp()这些函数在做响应式时特别好用。比如标题字号一行搞定.title { font-size: clamp(1.5rem, 3vw 1rem, 3rem); }这套逻辑写多了以后你会发现“预处理器”这件事本身是历史产物它解决的问题已经被浏览器一点点标准化掉了。caveman 项目里不装 Sass因为不需要。2.3 用原生 ES Modules 组织 JavaScript 逻辑JavaScript 端我直接用script typemodule浏览器原生支持 import/export不需要 webpack、rollup 这种打包工具。比如主入口script typemodule src/js/main.js/scriptmain.js里合并模块import { initNav } from ./modules/nav.js; import { initForm } from ./modules/form.js; import { initScrollEffects } from ./modules/scroll.js; document.addEventListener(DOMContentLoaded, () { initNav(); initForm(); initScrollEffects(); });每个模块互不干扰比如nav.jsexport function initNav() { const toggle document.querySelector([data-nav-toggle]); const menu document.querySelector([data-nav-menu]); if (!toggle || !menu) return; toggle.addEventListener(click, () { menu.classList.toggle(is-open); toggle.setAttribute(aria-expanded, menu.classList.contains(is-open)); }); }用原生模块有一个好处浏览器直接按 import 关系加载文件调试时 DevTools 里看到的文件名和源码完全一致不用费力去看压缩后的变量名。有一点必须注意原生 ES Modules 受 CORS 限制直接用file://协议打开 HTML 会报跨域错误。开发时必须跑一个本地静态服务器我一般会用 Node 自带的npx serve或者 Python 的python3 -m http.server这个后面部署部分会细讲。2.4 一个“组件”在 caveman 项目里长什么样没有框架不代表没有组件化的手段。caveman 里我推荐两种轻量组件写法实测下来都很稳。第一种是模板函数。一个卡片组件就是一个返回模板字符串的函数export function card({ title, text, link }) { return article classcard h3${title}/h3 p${text}/p ${link ? a href${link} classcard-link了解更多/a : } /article ; }在页面里用import { card } from ./modules/card.js; const list document.querySelector([data-card-list]); list.innerHTML [ { title: 第一项, text: 描述信息, link: /about.html }, { title: 第二项, text: 描述信息, link: null }, ].map(card).join();第二种是自定义元素。如果卡片需要内部事件、生命周期原生 Custom Elements 完全能打class CavemanCard extends HTMLElement { connectedCallback() { this.innerHTML article classcard h3${this.getAttribute(title)}/h3 p${this.getAttribute(text)}/p /article ; this.querySelector(.card).addEventListener(click, () { console.log(card clicked:, this.getAttribute(title)); }); } } customElements.define(caveman-card, CavemanCard);然后在 HTML 里直接用caveman-card title零依赖 text原生组件照样复用/caveman-card这两种方式都不会引入运行时依赖代码量少可读性高还保留了 HTML 本身的语义结构。3. 性能实测丢掉框架之后到底快了多少3.1 三个维度的对比数据我拿同一张产品落地页做过一次对比一个版本用 React Vite 构建另一个版本用 caveman 原生方案。两者实现同样的视觉和交互内容、图片、接口完全一致部署在同一家边缘节点上。两个版本的关键指标如下指标React Vitecaveman 原生方案页面资源总大小186KB含 JS 包和预构建缓存24KB首次加载 JS 执行时间模拟慢速 4G约 1.2s约 0.05s首屏 LCP 时间约 3.8s约 1.6s开发环境冷启动时间15~20s几乎 0s这个结果并不意外因为我不需要下载 React 运行时不需要下载路由库也不需要在首屏解析执行一大段框架初始化代码。注意我这里说的是同一页面的对比不是框架黑。React 处理复杂状态管理时优势很大但当一个页面只是展示内容加几个交互事件时框架运行时就是纯粹的负担。3.2 为什么原生方案在静态页面上有天然优势理解这个问题得先理清浏览器加载一个页面时发生了什么。浏览器拿到 HTML 后解析 DOM 和 CSS同时发现script标签就下载并执行。如果这个 JS 是一个 100KB 的框架运行时那它先得被下载、解析、编译再执行框架自身的初始化逻辑然后才轮到你的业务代码。caveman 方案里业务 JS 一共就几 KB还是按模块拆分、按需加载的浏览器没必要花大量时间在“消化框架”上。另外原生 CSS 早已支持变量、网格、弹性布局、动画、滚动驱动等能力很多效果以前要靠 JS 动画库现在几条 CSS 规则就够了。还有一点容易被忽略没有依赖就没有版本升级压力。传统项目两三个月不更新依赖重新打开就是一堆红色告警caveman 项目没有package-lock.json没有node_modules想更新都不知从哪里更新起这本身就是一种长期性能保障。3.3 Lighthouse 跑分结果我习惯用 Lighthouse 做基准测试页面跑完的分数是这样的Performance98Accessibility100Best Practices100SEO100满分当然有测试环境偏理想化的成分但“Performance 98”是实打实的丢掉的 2 分是因为第三方字体请求。如果连 Google Fonts 都不引Performance 可以稳定在 99 到 100。很多朋友问我为什么不用图片压缩工具、不用 JS 代码压缩工具答案很简单我把 HTML、CSS、JS 都写成精简版本同时配合服务器 gzip这已经足够。对静态内容型站点来说压缩工具带来的收益边际已经很小但维护成本、工具链复杂度反而上去了。4. 发布与维护没有构建工具的部署流程怎么做4.1 静态托管怎么选caveman 项目的部署是它最大的快乐源泉。因为源码就是部署源不需要跑npm run build不需要上传 dist 目录直接把整个项目目录扔到任何一个静态文件服务里就能跑。我用过几个方案平台自带的静态托管比如 GitHub Pages、Cloudflare Pages、Netlify支持直接连仓库推代码即上线。对象存储 CDN适合有自定义域名和团队已有云服务的场景把文件传到桶里配上 CDN 加速即可。云服务器上跑 Nginx适合同时要处理服务端转发、旧项目共用的场景。这几个方案对 caveman 项目来说没有本质区别因为最终都是把静态文件放到一个能对外访问的地址。我个人的习惯是个人小项目放在 GitHub Pages 上公司项目放对象存储因为内容可控、回滚方便。4.2 手动部署 vs 自动部署没有构建工具不代表不能自动化。我的做法是保留一个极简的发布脚本比如在项目根目录放一个deploy.sh#!/bin/bash set -e # 如果你用对象存储这里就是同步命令 # 如果你用 Pages 平台其实只需要 git push # 本地服务预览确认无误后执行同步 echo 开始同步到线上... # 示例用 rclone 同步到存储桶按自己的工具替换 rclone sync ./assets/ your-bucket:assets/ --progress rclone sync ./pages/ your-bucket:pages/ --progress rclone sync ./index.html your-bucket:/ --progress echo 部署完成如果你直接使用 Cloudflare Pages / Netlify / GitHub Pages部署甚至不需要脚本设置好“项目根目录”后每次git push就自动上新版本。这里我反而建议别把流程搞复杂。caveman 的意义就在于减负如果为了部署再引入一套 CI 编排、十几个依赖的自动化流程那就又绕回去了。手动跑一条三行命令的脚本或者直接推个 commit已经足够高效。4.3 本地预览与调试本地开发时最需要具备的条件是“一个日志输出干净、刷新即更新的环境”。caveman 项目推荐用以下方式如果机器上有 Node直接执行npx serve .默认会在 3000 端口起一个静态服务器。如果机器上有 Python执行python3 -m http.server 8080也能达到同样效果。想实现浏览器自动刷新可以装一个 livereload 扩展但我实测下来更简单的方式是改完代码按一下 Cmd/CtrlR网站是静态的刷新根本不需要几秒。有一点我踩过坑只改 CSS 时不要强制刷新整个页面直接在用 DevTools 的 Network 面板里勾选“Disable cache”并开着 DevTools 实时预览效率会高很多。5. 项目扩展在极简基础上加哪些“不脏”的优化5.1 图片懒加载与响应式规格在原生环境里图片优化有最好的工具浏览器已经原生支持loadinglazy、decodingasync和srcset。示例写法img srcphoto-small.jpg srcsetphoto-small.jpg 600w, photo-medium.jpg 1200w, photo-large.jpg 1800w sizes(max-width: 600px) 100vw, 1200px alt示例场景 loadinglazy decodingasync /loadinglazy让视口外的图片延迟加载srcset让浏览器按当前屏幕宽度选合适尺寸。这些能力以前都要用懒加载库实现现在全部原生支持而且不会影响 SEO。如果项目里有很多重复的缩略图我会用 shell 脚本或者在线工具批量生成多尺寸版本命名规则统一为xxx-600.jpg、xxx-1200.jpg这种避免每次手工改 HTML。5.2 性能优化三件套preload、preconnect、defer没有构建器的另一个好处是你能精确控制关键资源加载。一般我会在head里加上这几行link relpreconnect hrefhttps://fonts.example.com crossorigin / link relpreload href/assets/css/base.css asstyle / link relpreload href/assets/images/hero.webp asimage typeimage/webp /preconnect提前和第三方域名建立连接减少字体、CDN 资源等待preload让 CSS 或者首屏关键图片优先下载不走普通解析顺序script defer让 JS 延迟到 DOM 解析完成后执行避免阻塞渲染。这些手段在构建工具里往往被自动处理但自动处理的结果经常是“全部打包、全部预加载”反而不精细。手写反而每一条都是有用功。5.3 语义化 HTML 与 SEO 优化很多人以为 SEO 优化需要一堆插件其实原生语义化 HTML 就是最好的 SEO 基础。我的 caveman 页面里会严格使用header、nav、main、article、section、footer并且保持标题层级清晰。另外加一套简单的 SEO 头部信息title我的产品 | 一句话介绍/title meta namedescription content用最少的资源做最快的落地页。 / meta propertyog:title content我的产品 / meta propertyog:type contentwebsite / meta propertyog:url contenthttps://yourdomain.com/ / meta propertyog:image contenthttps://yourdomain.com/og-cover.png / link relcanonical hrefhttps://yourdomain.com/ /再加上一个.htaccess或平台自带的缓存规则对静态资源做长缓存。这样 SEO 健康度和访问速度都可以同时保证。5.4 如果想要一点“手感”引原生 API 就够了交互效果也是一样优先用原生能力。比如滚动出淡入效果可以用IntersectionObserver30 行代码搞定不用引 AOS。回到顶部一行 JS 加一个按钮配合 CSSscroll-behavior: smooth。背景视差用background-attachment: fixed在一个装饰元素上实现不需要 skrollr。手风琴折叠原生details/summary直接解决零代码。弹窗原生dialog标签配合一点 CSS 就能做出好看的模态框。我实测下来很多第三方库提供的“小交互”用原生 API 实现都不超过 50 行。写起来虽然偶尔多几行 CSS但换来的是零依赖和加载速度上的碾压。6. 踩坑清单与一些实际心得6.1 原生 ES Modules 在 file:// 协议下的限制新手用 caveman 方案最容易踩的坑是把index.html直接双击打开然后看到CORS policy报错。这不是代码问题而是浏览器安全策略禁止通过 file 协议加载 ES Modules。解决方案很简单起一个本地静态服务器前面已经给过命令。我建议不要用 IDEA 自带的“直接在浏览器打开”按钮而是养成用npx serve .的习惯这也是以后线上部署环境最接近的一种模拟。6.2 浏览器兼容性的取舍零依赖有个隐形成本你得自己决定“支持哪些浏览器”。比如 CSS 嵌套、color-mix()、dialog、showModal都不是老浏览器能用的。我的做法是项目默认只支持最近两个大版本的现代浏览器Chrome、Edge、Firefox、SafariCSS 用supports做能力检测老浏览器优先保证核心内容可达但不追求视觉效果完全一致关键交互表单提交、导航不做成“只有新浏览器才能用”而是把基本 URL 跳转作为降级方案这种取舍其实是很多商业项目已经在做的事只是以前藏在构建器的 targets 配置里。在 caveman 项目里这条原则写在项目 README 的第一行谁接手都不会误解。6.3 什么时候应该主动放弃“穴居人”这个标题听起来有点反主流的味道但我自己并不是无脑抵制框架的人。caveman 方案在项目变得复杂时有一个很明显的临界点当我发现自己开始在 JS 里手工实现响应式状态管理或者在写一堆组件通信逻辑时我就会停下来认真考虑要不要引入一个轻量框架。判断标准其实就一个代码里有没有大量“元素之间的跨区块数据联动”如果有说明业务逻辑已经很复杂再硬扛原生方案显得有点强迫症了。这时候换成 React/Vue/Svelte 都是合理选择。caveman 的低成本优势只有在简单场景里才成立。6.4 实际维护体验三个月后再打开项目这套方案用了一段时间后最大的感触是“遗忘成本降到了零”。最近我打开一个三个月没碰的 caveman 项目没有 node_modules没有 lock 文件也没有“记得先 npm install”的流程。打开 README五分钟之内就搞清楚了全部结构改了一个按钮的颜色推到线上完事。如果是传统构建项目三个月后先要 npm install等依赖装完再升级一些被安全扫描标记的包然后再看构建是否通过。这会消耗大量心力而且和你要改的那个按钮颜色毫无关系。如果让我总结 caveman 这个命名背后真正的含义就是一句话用最接近本质的方式解决简单问题同时保留随时退出的自由。它不是什么银弹但对我这种经常要维护多个小项目的人来说它提供了一个很舒服的中间地带——不需要的东西一开始就不应该存在。以后我再做活动页或产品落地页大概率还是会打开这个“原始人”模板因为它简单到几乎不会出错。