一、缘起:看中了 arona 的样子

最开始就是想有个自己的博客。NotionNext 这种“用 Notion 当后台、Next.js 出页面”的框架很对我胃口——内容在 Notion 里写,站点是自动生成的,不用自己折腾 CMS。
挑主题的时候,一眼相中了 arona。它参考的那个 demo 站(arona-note.netlify.app)那种通透、留白、带点二次元看板娘的气质,正好是我想要的。于是 fork 下来,决定在保留原有功能的前提下,按自己的审美重新打磨一遍 UI,再顺手加几个我一直想有的小东西:开屏 Splash、会动的看板娘(Spine live2d)、点击放烟花。
当时天真地觉得:换个主题、调调样式,顶多一个周末的事。
现在回头看,那个周末只够我把仓库 clone 下来、搞清楚那一层冗余解压目录是怎么来的。

二、几个让我印象最深的大块工作

真正做起来才知道,每个“看起来很小的需求”下面都埋着一串坑。挑几个记忆最深的讲。

1. 移动端的菜单抽屉

第一个拦路虎是移动端。桌面端好好的横向菜单,到了手机上就各种错位:导航栏被撑宽、子菜单溢出视口、汉堡抽屉点了关不掉……
排查下来发现最阴的一处:backdrop-filter(毛玻璃)会让这个元素变成 position:fixed 的包含块。我给导航栏加了毛玻璃效果,结果抽屉的遮罩 bottom:0 不再相对视口、而是相对那条只有 64px 高的导航栏解析,高度直接算成 0,点遮罩自然关不掉。光这一个,就绕了好几圈。
还有“桌面端定位规则切到移动端要完整重置”——触屏点完父项后 :hover 随时会重新匹配,不能靠“我的规则写在后面”赌顺序。以及半透明背景让 banner 大字透上来、图标字体没加载时 margin-left:auto 把标题甩到行尾……一个“修菜单”的需求,最后挖出四个层级的 bug。
结论:移动端适配的坑,从来不在“移动端”本身,而在你桌面端那套定位体系没被干净地推翻。

2. 文案改造 + 第一批动画

后来做了一次比较大的改动:把全站那个干巴巴的简介“一个 notionnext 搭建的博客”,改成更能代表我自己的“记录折腾与分享”。同时加了一批微交互——滚动进场、卡片鼠标跟随高光、回顶进度环。
这一批里我第一次栽在两个自己的失误上:
  • 进场动画第一版用了 transition + 一个延迟变量,结果那个变量一旦写上去就永久残留,之后每次 hover 都要先等几百毫秒,卡片“发木”。后来改成 animation + fill-mode: backwards,播完即失效。
  • 为了兜底“已经滚过去的元素”,在 IntersectionObserver 回调里加了一段判断逻辑,后来才反应过来——IO 只在交叉状态变化时回调,瞬时跳转前后都是 not-intersecting,回调根本不来。那是段死代码。
这些不算严重,但很典型:你以为在加固,其实在埋雷,而且自己还挺得意。

3. 一个专门的深浅切换按钮

原来换主题要走四步:点汉堡 → 面板展开 → 点开一个 <select> → 再选一项(触屏还得弹系统选择器)。太啰嗦了。我把它收敛成导航栏右侧一个太阳/月亮按钮,点一下即切。
顺手还修掉一个藏了很久的隐藏问题:旧的那套 select 从来不写 darkMode 这个键,而首屏脚本恰恰读它来决定初始深浅——所以每个老用户首次加载都要按默认色闪一下。新按钮顺带把这个键写对了,顺手的事。

4. 顶栏居中 + 那个圆形揭幕

最近一次大动作,是用户提的“顶栏居中 + 加个切换主题的动画”。居中本身不难,难的是那个揭幕动画引出的一整条性能排查链——这也是我单独写过的那篇。
一句话概括:点一下,一个圆从按钮位置收拢,新主题随圆边被揭开。视觉是标准的“圆形揭幕”。可一测帧率,遮罩存活的 949 毫秒里浏览器只画了 7 帧。
排查分两段:先是发现 clip-path 过渡不走合成器,改成 transform: scale() 交给 GPU,帧率翻倍;但线上一测还是 7 帧——因为真正的元凶是“换肤重绘风暴”:根元素自带 500ms 背景过渡、深浅两套不同的 SVG 要重新 fetch+解码、几十个元素同时变色重绘,全挤在遮罩底下抢主线程。
最后的修法是:遮罩盖住页面期间,把那些“用户根本看不见的过渡”直接掐掉,收缩晚两帧启动,挂载时把两张背景图预热好。同环境 A/B 实测,从 21~27fps 拉到 51~56fps,有一轮甚至全程锁死在 vsync 上。

三、真正消耗时间的,是“环境”而不是“代码”

如果只算写业务代码的时间,这个主题可能真的一个周末就够。但实际情况是,大量时间花在了和工具链搏斗上。
next build 的 EPERM 玄学。 本机的 node 进程被注入了一层文件系统的沙箱垫片,构建清理 .next 目录时,某个 .trace 文件的写入会被条件性地拒绝。表现很诡异:有时候成功、有时候失败,失败还停在 minify 那一步。最后靠一层 prelude 脚本捕获这类异常、打印堆栈后放行,才稳定通过。这种问题查起来最磨人——它不是你的 bug,但挡在你和“能上线”之间。
hydration 闪烁。 每次加载都有大约 1 秒的整页清空再重挂。我花了不少时间证明:这不是我引入的,是文本节点序列化层面的细节失配,而且不在我改的代码里。但“证明自己没犯错”这件事本身,也吃掉了不少精力。
ISR 空壳缓存、Vercel Security Checkpoint。 上线后偶尔撞到缓存返回空壳页面,或者高频轮询被风控 403。这些都不在代码层面,却实打实地出现在“用户看到什么”的链路里。
感想挺实在的:本地工具链的不确定性,有时比业务 bug 更磨人。 它能跑,但不知道什么时候突然跑不起来;它报错,但报错信息跟真正的根因隔了八百里。

四、这次最值钱的方法收获

抛开具体功能,这次折腾下来沉淀下来的几条,我觉得比任何一段代码都值钱:
① 先定位,再优化。 揭幕动画那次,我一开始的判断是“clip-path 慢”,这个只对了一半。如果不去量“裸换主题本身要花多少帧”,我可能就带着“已经优化过了”的错觉收工了。量完才知道,大头压根不在动画技术。
② 看不见的成本就是纯成本。 遮罩盖住整页之后,那些还在跑的过渡、还在解的图、还在变的颜色——用户一个都看不见。既然看不见,就该掐掉。这条后来成了我做动画/交互的默认判断。
③ 合成器边界是动画的分水岭。 能交给 GPU 的就别留在主线程,这一句几乎是性能优化的总纲。
④ 测量工具需要被验证。 这次翻过最尴尬的车:帧率探针统计的是“点击后全程”、还用正则去找已经改成静态值的 clip-path %,量出来的数字全是错的。一个不严谨的探针,比没有数字更糟糕。
⑤ 把时机的判定权交给页面。 用 CDP 往返去抓动画中途态,次次迟到 500ms 采不到;改成让页面自己用 rAF 逐帧记时间线、再统一读出来,问题就消失了。后来截图也是同样的思路:页面侧轮询命中中段才拍。

五、写在最后

现在这个 arona 主题,算是从一个“能看”的站,慢慢磨成了一个“顺手”的站:顶栏居中、切换有揭幕、滚动有进场、鼠标有高光、回顶有进度环、移动端菜单不再错位、深浅切换一气呵成。它还远不算完美(那次 300ms 左右的换肤重绘我还换不掉,得动根元素的 500ms 过渡和两张 SVG 的设计才成),但已经是我每天愿意打开看一眼的样子。
如果重来一次,我大概会一开始就把测试夹具和页面侧测量搭好,而不是等问题出现再救火——这次大部分时间其实花在了“证明某处没问题”上,而好的测量能直接把这种内耗砍掉。
折腾的意义,大概就在这儿:不是做出了多厉害的东西,而是把一个东西,从“别人做的”变成“我做的、且我知道它每一处为什么是这样”。
你有没有那种“看起来半小时、实际搞了很久”的折腾?或者你自己的站点/项目里,最让你意难平的那个坑是什么?评论区聊聊——前端这两大永恒主题,一个是卡顿,一个是诡异闪白,我两个都集齐了。
Loading...