本文记录了一个大二学生在开发校园论坛时遇到的真实线上问题,以及从 Lighthouse 80 分一步步优化到 100 分的完整过程。如果你也在独立做全栈项目,希望这些坑和经验对你有帮助。
项目背景
我是一名计算机专业大二学生,独立开发了一个面向全校的校园论坛。
技术栈 :Vue3 + Vite + Pinia(前端) + Express + MongoDB(后端) 部署架构:Vercel(前端) + Railway(后端) + Cloudflare(DNS 与 CDN)
项目刚上线时,首页加载速度很不理想------用 Lighthouse 一跑,Performance 只有 80 分,First Contentful Paint 高达 3.6 秒。对一个面向全校学生的论坛来说,这个体验显然不行。
下面是我从发现问题到逐一解决的完整过程。
一、移动端深色模式异常:CSS 变量控制权之争
问题现象
论坛支持手动切换深色/浅色主题,用 Pinia Store 管理状态并持久化到 localStorage。桌面端测试一切正常,但用手机浏览器打开时,深色模式下的颜色完全错乱------该亮的地方暗了,该暗的地方亮了。
排查过程
刚开始怀疑是 CSS 变量写错了,在桌面端反复切换都没问题。后来无意中在手机 Chrome 里关掉了系统深色模式,论坛颜色居然变正常了。
这才意识到:不是我的代码逻辑错了,而是浏览器系统设置和我的手动切换在打架。
根因分析
我的 CSS 里同时存在两套深色判断机制:
- CSS 媒体查询 :
@media (prefers-color-scheme: dark)监听系统偏好 - JS 手动切换 :通过
document.documentElement.setAttribute('data-theme', 'dark')控制
当手机系统处于深色模式时,媒体查询自动生效,覆盖 了我手动设置的 data-theme="light"。两套规则同时作用,颜色就乱了。
解决方案
既然 JS 已经完全接管了主题切换,CSS 媒体查询就成了干扰项。我把所有 @media (prefers-color-scheme: dark) 块里的变量迁移到 [data-theme="dark"] 选择器下,然后删掉媒体查询块。
同时改进了初始化逻辑,让系统偏好只在用户首次访问时作为默认值,之后完全由用户手动控制:
javascript
const saved = localStorage.getItem(STORAGE_KEY)
const getSystemPreference = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const theme = ref(saved || getSystemPreference())
额外发现
修完后手机端仍有异常,排查发现是部分安卓自带浏览器 (如华为、小米浏览器)有"强制深色模式",会无视网站的 data-theme 属性直接反转颜色。解决办法是在 index.html 里加 meta 标签明确告诉浏览器"我的网站已支持深色模式,请不要自作主张":
html
<meta name="color-scheme" content="light dark" />
教训:前端状态管理要保证"单一数据源"原则。主题切换要么全交给 CSS 媒体查询,要么全交给 JS,混用必然出问题。
二、首页性能优化:从 Lighthouse 80 到 100
第一次跑 Lighthouse
优化前跑了一次,报告显示:
- Performance:80
- FCP(首次内容绘制):3.6s
- LCP(最大内容绘制):3.6s
- Speed Index:4.5s
关键瓶颈:Render-blocking requests,一个 2.3KB 的 CSS 文件阻塞了首屏渲染,预估可节省 890ms。
第一步:内联关键 CSS
既然是渲染阻塞,最简单的办法就是把阻塞的 CSS 直接内联到 HTML 里,让浏览器拿到 HTML 就能渲染,不用再等额外的网络请求。
我打开 Vite 构建后的 dist/assets/index-xxx.css,把全部内容复制进了 index.html 的 <style> 标签。由于文件只有 8KB 左右,内联不会明显增加 HTML 体积。
但这里踩了一个坑:Vite 构建后 CSS 文件依然存在,我以为内联失败。后来才明白,Vite 仍输出完整 CSS 给非首屏路由使用,但浏览器已经不会被它阻塞首屏渲染了。
优化后 FCP 明显下降,Performance 从 80 飙到 97。
第二步:Preconnect 预连接后端
97 分已经不错了,但请求链分析显示,页面必须等 JS 下载并执行后,才发起 /api/posts 请求获取帖子列表。这三个步骤是串行的。
解决方案:在 HTML 头部加一行 <link rel="preconnect">,让浏览器在解析 HTML 时就提前和后端服务器握手,等 JS 发起请求时 TCP 连接已经就绪:
html
<link rel="preconnect" href="https://forum-project-production.up.railway.app" />
这一步让关键路径延迟从 3055ms 降到 2355ms。
第三步:Cloudflare 缓存 API 响应
此时 /api/posts 有约 2 秒的响应延迟,这是 Railway 后端本身的响应时间 + 物理距离,前端已经无能为力。
我用 Cloudflare 的 Cache Rules 为 /api/posts 配置了 2 小时的边缘缓存。这样同一个接口在缓存有效期内直接从 Cloudflare 全球节点返回,响应时间降到 100ms 以内。
注意:缓存规则只对走你域名的请求生效。如果你的 API 请求直接指向 Railway 后端(跨域绝对路径),Cloudflare 缓存完全用不上。这一点在下一步踩坑中暴露了出来。
第四步:统一 API 路径 ------ 踩了最大的坑
之前的 API 请求全部用的 Railway 绝对地址:
javascript
fetch('https://forum-project-production.up.railway.app/api/posts')
这导致所有请求绕过 Cloudflare,缓存规则形同虚设。于是我改成了相对路径:
javascript
fetch('/api/posts')
但改完之后本地开发环境直接报错 :Unexpected end of JSON input。
原因:相对路径在本地会请求 localhost:5173/api/posts,而 Vite 开发服务器上根本没有这个接口。
解决:在 vite.config.js 里配置代理,让开发环境也自动转发 /api 请求到 Railway:
javascript
server: {
proxy: {
'/api': {
target: 'https://forum-project-production.up.railway.app',
changeOrigin: true
}
}
}
同时创建 vercel.json 为生产环境配置 Rewrite 代理:
json
{
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://forum-project-production.up.railway.app/api/:path*"
}
]
}
又一个坑 :最开始把 vercel.json 放在了 src/ 目录里,部署后接口一直 404。折腾半天才发现这个文件必须放在项目根目录。
修完之后,Performance 终于冲到了 100。
三、发帖体验优化:从"发出去了吗"到"发布成功"
原来的流程
发帖 → 调用 API → 清空表单。没了。没有反馈,不知道成功了没,也不知道帖子在哪。
优化后的流程
发帖 → 等待后端返回 → 弹出成功卡片 → 2 秒倒计时 → 自动跳转首页 → 新帖出现在列表顶部。
具体改动
usePostsStore 的 addPost 函数需要 return newPost,让组件能拿到新帖的 _id。
发帖组件增加三个状态:
submitting:控制按钮禁用和文字切换showSuccessCard:控制成功弹窗countdown:倒计时秒数
核心逻辑:
javascript
async function submit() {
if (!content.value.trim() || !title.value.trim()) return
submitting.value = true
try {
await postsStore.addPost(content.value, title.value)
// 清空表单、弹成功卡片、倒计时跳转
} catch (err) {
alert('发布失败:' + err.message)
} finally {
submitting.value = false
}
}
另外两个小细节
- 新帖排在最前面 :
push改成unshift,让新帖插入列表头部而非尾部。 - 跳转时滚到顶部 :在 Vue Router 里加
scrollBehavior() { return { top: 0 } },避免跳转后停在旧滚动位置。
四、总结与感受
这次优化踩了太多坑:CSS 变量控制权冲突、手机浏览器强制深色、Cloudflare 缓存不生效、vercel.json 位置放错、API 路径改动导致本地崩溃......每一个坑查出来都要花很久,但查出来那一刻的成就感也是真实的。
分享几点体会:
- Lighthouse 跑分不是玄学,报告里清清楚楚写着瓶颈。 先跑分再优化,不要盲目猜。
- 单一数据源原则不只是口号。 主题切换上的混乱就是典型反面案例。
- 部署和构建相关的配置文件要搞清楚放哪。
vercel.json放错目录这种事情,文档不会特意强调,但错了排查起来很痛苦。 - 本地和生产环境要尽量对齐。 这次
vite.config.js和vercel.json分工协作,才让开发和生产都正常工作。
如果你也在做独立项目,欢迎评论区交流你的踩坑经历。