三年搬完一次样式迁移,值钱的是那张分期表

一、代价是在规模上去之后才开始收的
一个大型网站把整站样式从运行时方案搬到编译期方案,用了三年。这类复盘值得抄的不是它的技术选型,是它把“两套并存三年”这件事当成一个需要秩序的状态来处理。先说清代价的来源:运行时样式方案在组件数不多时几乎感觉不到,这不是它好或坏的证据,只是规模没到。组件数量与嵌套深度是乘数关系,三类代价都带乘数。
- 首屏与交互的运行时开销:类名在浏览器侧生成并注入,样式规则在运行期拼装,这笔成本按渲染路径付,而且每次付。
- 优先级不可预测带来的调试成本:作用域由生成顺序与注入时机决定,同一个选择器在不同上下文里可能给出不同结果,定位一次覆盖问题要先复原注入顺序。
- 每层组件都要背一份类名生成逻辑:外层多包一层,内层的样式关联就可能失效,样式责任从“写在哪儿”变成“谁先生成”。
于是“要不要搬”的判断必须基于增长而不是现状:看组件数的增长斜率和嵌套深度的实际分布,而不是看这一段时间里有没有卡顿投诉。规模还在爬坡的项目按现状判断会得出“不用动”,而这条结论的有效期只到拐点那天。反过来说,规模永远不大的项目留在原方案里也完全成立,本篇不是在比较两种方案谁更高明。
二、三种终点里只有第三种算搬完
这个区分听着像抠字眼,实际决定排期和资源投放。
- 新方案可用:能力就绪,样板页面跑通。此时项目的成本表上多了一套要维护的东西。
- 新方案覆盖全部页面:页面都走新方案,但旧依赖还在依赖树里,仍被构建,仍要有人回答“为什么还没删”。
- 旧依赖被彻底删除:新增代码没有第二条路可走,旧方案不再存在于构建产物里。
只有第三种是终点,前两种都还留着成本,留的还是隐性成本:两套都活着的时候,任何人写新代码都要重新问一次“用哪个”。三年这个跨度之所以值得复盘,不是因为慢,是因为它真的走到了第三种;大量长期迁移停在第一种或第二种,然后被反复重启,每一次重启都再付一遍学习成本。
三、分期表的四种切法与各自的风险
| 切法 | 适用条件 | 会踩的坑 |
|---|---|---|
| 按路由切 | 路由边界清晰,页面之间耦合低 | 同一页面内两套并存,公共组件被两边同时引用 |
| 按团队边界切 | 团队与页面归属能对上 | 交界处撞车,共享组件被两方按不同约定各改一遍 |
| 按组件库切 | 组件是复用的最小单元 | 漏掉使用方:库换完了,页面里的旧写法还在生效 |
| 按“新写的走新、存量不动”切 | 想先止住新增 | 旧区域永远搬不完,存量占比只降不消 |
四种切法都能推进,没有哪一种自带正确答案;第四种看着最没有成本,也最容易变成没有终点。这套分类来自别人的复盘,不能当成普适标准,选型依据是自己项目的耦合形状。
每种切法都要配三条护栏,缺一条就会失控:
- 新增代码走哪套要有强制规范,并由静态检查或评审拦下来,不能靠口头约定和新人文档。
- 每一块区域要有写明完成判据的定义,达标才登记,没达标不许标成“进行中”。
- 双轨期间的回归范围要单独界定:一块搬完,回归不只测这块,还要测与它共用组件的邻区。
护栏里第一条最容易漏掉的是“谁”。双轨期真正需要的是一份区域归属登记,每一块写明当前走哪套、下一块由谁负责搬、这块的新增代码由谁判定合规。没有归属登记时,“新写的都走新”这条规则会在交界处失效:一次改动横跨两块区域,两块各有约定,最后哪一边都不认这笔账。登记表不必复杂,四列够用——区域、当前形态、负责人、下一块的完成判据。它的价值不在管理,在于把“这块归谁”从每次争论变成一次查询。
四、“这块搬完了”要四条判据齐了才算
判据必须能被别人复核,所以四条都得是可检查项,不是主观印象。
- 该区域内不存在任何旧方案的引用,扫描能给出结果,不接受“我看过了”。
- 样式冲突用例通过,把选择器优先级、全局样式与层叠顺序这几类冲突写成用例并跑过。
- 性能指标不低于基线,基线要在动手之前于旧方案上取好,搬完再对照;先搬后测等于没有对照。
- 视觉回归无差异,有差异的要么消除,要么登记为有意的样式修正并留下说明。
四条齐了才登记完成,缺一条就写未完成。这条纪律看着严格,实际是在保护后面的人:一个被误标完成的区域会以“已完成面积”的形式进入进度汇报,最后变成谁都不敢再碰的死角,重做的成本比当初严格按判据走要高得多。
五、双轨期的三类事故与删依赖前的两道闸
并存期间出的事故集中在三处。同名选择器互相覆盖:两套方案对同一个类名各写了一份,最终生效的取决于加载顺序。全局样式被旧方案注入:运行时的全局写入不在新方案的静态输出里,代码里找不到出处,只能在产物里反查。动态类名在编译期方案里失效:凡是运行时拼字符串生成类名的写法,编译期提取时看不见那个类名,它不会进入生成的样式表。第三条尤其隐蔽,因为典型表现是“本地跑一切正常”,只有拼接结果恰好没被提取到时才现形,而这种情况往往要到某个很少被访问的分支里才发生。
删旧依赖前必须过两道闸。第一道是一次引用计数归零的静态扫描,扫描面要覆盖源码、配置文件、构建脚本以及动态字符串这几类;第二道是在灰度期间保留一次可回退的构建配置,保留到确认删除之后没有回归为止。第二道闸的作用不是回退代码,而是让“删依赖”这个动作本身可以被安全地重做——一旦删完发现漏了引用,能恢复原状的成本比重新排查低。
六、三年跨度的管理要点,和真正能带走的三件东西
跨度以年计的迁移,管理上真正会失效的是两件事。
- 每半年重估一次收益。运行时方案的性能问题可能已经被别的优化解决了,当初的判断标准会变;重估的结论可以是继续,可以是换切法,也可以是停下来。
- 把“搬完”与“业务功能交付”的排期分开。混在同一条排期里,迁移永远是被挤掉的那一项,因为它没有截止日,而功能需求有。
一条纪律值得单独写出来:如果一块迁移在可见的几年内都看不到删旧依赖的机会,它应该被重评为“不搬”,用一个兼容层把它固化下来,而不是继续拖着。半死不活的双轨比彻底不搬更贵,它同时付两套的成本,却不产出终态。
这套方法是可迁移的。日志框架、状态管理、请求层、埋点 SDK 这几类长期替换,形状完全一样:都满足“新能力可用不等于搬完”,都要靠分期表、完成判据和删除闸门推进。差别只在耦合面——请求层的耦合散在调用点,状态管理的耦合压在数据结构上,所以要按各自的耦合形状重选切法,而不是照抄样式迁移的顺序。
客户侧真正能交付的东西就三件:一张分期表、每块区域的四条完成判据、一份双轨期风险与护栏清单。洹拓接前端长期替换类项目时按这三件出文档,因为进度争议从来不是“做了多少”,而是“这块到底算不算完”。