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 type="module">,浏览器原生支持 import/export,不需要 webpack、rollup 这种打包工具。比如主入口:
<script type="module" src="/js/main.js"></script>main.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.js:
export 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 class="card"> <h3>${title}</h3> <p>${text}</p> ${link ? `<a href="${link}" class="card-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 class="card"> <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 + Vite | caveman 原生方案 |
|---|---|---|
| 页面资源总大小 | 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 做基准测试,页面跑完的分数是这样的:
- Performance:98
- Accessibility:100
- Best Practices:100
- SEO:100
满分当然有测试环境偏理想化的成分,但“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/Ctrl+R,网站是静态的,刷新根本不需要几秒。
有一点我踩过坑:只改 CSS 时不要强制刷新整个页面,直接在用 DevTools 的 Network 面板里勾选“Disable cache”并开着 DevTools 实时预览,效率会高很多。
5. 项目扩展:在极简基础上加哪些“不脏”的优化
5.1 图片懒加载与响应式规格
在原生环境里,图片优化有最好的工具:浏览器已经原生支持loading="lazy"、decoding="async"和srcset。
示例写法:
<img src="photo-small.jpg" srcset="photo-small.jpg 600w, photo-medium.jpg 1200w, photo-large.jpg 1800w" sizes="(max-width: 600px) 100vw, 1200px" alt="示例场景" loading="lazy" decoding="async" />loading="lazy"让视口外的图片延迟加载,srcset让浏览器按当前屏幕宽度选合适尺寸。这些能力以前都要用懒加载库实现,现在全部原生支持,而且不会影响 SEO。
如果项目里有很多重复的缩略图,我会用 shell 脚本或者在线工具批量生成多尺寸版本,命名规则统一为xxx-600.jpg、xxx-1200.jpg这种,避免每次手工改 HTML。
5.2 性能优化三件套:preload、preconnect、defer
没有构建器的另一个好处是,你能精确控制关键资源加载。一般我会在<head>里加上这几行:
<link rel="preconnect" href="https://fonts.example.com" crossorigin /> <link rel="preload" href="/assets/css/base.css" as="style" /> <link rel="preload" href="/assets/images/hero.webp" as="image" type="image/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 name="description" content="用最少的资源做最快的落地页。" /> <meta property="og:title" content="我的产品" /> <meta property="og:type" content="website" /> <meta property="og:url" content="https://yourdomain.com/" /> <meta property="og:image" content="https://yourdomain.com/og-cover.png" /> <link rel="canonical" href="https://yourdomain.com/" />再加上一个.htaccess或平台自带的缓存规则,对静态资源做长缓存。这样 SEO 健康度和访问速度都可以同时保证。
5.4 如果想要一点“手感”,引原生 API 就够了
交互效果也是一样,优先用原生能力。比如:
- 滚动出淡入效果:可以用
IntersectionObserver,30 行代码搞定,不用引 AOS。 - 回到顶部:一行 JS 加一个按钮,配合 CSS
scroll-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、Safari)
- CSS 用
@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 这个命名背后真正的含义,就是一句话:用最接近本质的方式解决简单问题,同时保留随时退出的自由。它不是什么银弹,但对我这种经常要维护多个小项目的人来说,它提供了一个很舒服的中间地带——不需要的东西,一开始就不应该存在。以后我再做活动页或产品落地页,大概率还是会打开这个“原始人”模板,因为它简单到几乎不会出错。