凌晨一点,林晨还在公司。
办公室的灯一排一排灭掉,只剩他这一片亮着。屏幕上,Chrome DevTools 的 Performance 面板刚刚跑完一次录制,时间轴被红色和黄色填满。
他盯着那条明显的紫色长条——
Recalculate Style + Layout。
“果然。”
章节内容一多,DOM 节点数量失控,哪怕只是改个字号,整棵渲染树都会被牵着跑。
他打开代码,把核心逻辑单独抽了出来。
watch(
() => readerSettings,
() => {
// 更新阅读样式
applyReaderStyle()
},
{ deep: true }
)
这段代码本身没错,但deep watch 就像一把钝刀——
方便、好用,但每次都伤筋动骨。
林晨把手放在键盘上,没有立刻改。
他在想的不是“怎么写”,而是——
要不要现在写得这么“重” 。
第二天早上,站会气氛不太轻松。
“阅读器这块,我们是不是可以直接用现成的虚拟列表库?”
后端同事随口提了一句。
苏雨点头:“我也觉得,用成熟方案更稳。”
林晨却摇了摇头。
“列表库解决的是固定高度或可预估高度的问题,”
他解释得很慢,“但章节是富文本,图片、标题、段落高度都不确定。”
他走到白板前,画了个简单示意。
“如果强行套库,要么牺牲滚动连续性,要么引入大量占位计算,反而更复杂。”
苏雨看着白板,皱眉:“那你打算自己写?”
“写一层轻量的虚拟渲染。”林晨说。
会议室里静了一下。
陈浩抬头:“风险?”
“有。”
林晨没有回避,“但可控,而且能针对我们的场景优化。”
午后,争论升级了。
苏雨翻着原型图:“我担心你们工程这边,一旦开始自研,就会不断往‘技术最优’走,最后体验被牺牲。”
林晨听出来了——
她说的不是代码,是不信任。
“我不追求最优。”他说,“我追求可解释。”
“什么意思?”
“当用户卡顿时,我能清楚知道卡在哪。”
“而不是被库封装住,只能祈祷。”
这一次,他的语气比之前更坚定。
傍晚,林晨开始真正落代码。
他没有一开始就“虚拟化一切”,而是选了最保守的切入点:
只虚拟不可见章节。
const VISIBLE_BUFFER = 2
function getRenderRange(currentIndex, total) {
const start = Math.max(0, currentIndex - VISIBLE_BUFFER)
const end = Math.min(total, currentIndex + VISIBLE_BUFFER)
return { start, end }
}
不是炫技。
只是把“用户永远只看当前章节附近内容”这个业务事实,变成了代码约束。
真正的渲染逻辑被包在一个计算属性里:
const renderChapters = computed(() => {
const { start, end } = getRenderRange(currentIndex.value, chapters.length)
return chapters.slice(start, end)
})
DOM 数量瞬间降了一个数量级。
他刷新页面,滚动。
顺了。
但问题没有结束。
设置面板一动,依然会触发整块内容重算。
林晨盯着 watch 那一行代码,敲下了删除键。
他改成了拆分状态 + 精准依赖。
const fontSize = computed(() => readerSettings.fontSize)
const lineHeight = computed(() => readerSettings.lineHeight)
watch(fontSize, updateFontSize)
watch(lineHeight, updateLineHeight)
没有 deep,没有魔法。
只是让变化只影响该影响的部分。
这不是技巧,是前端最基础的原则之一:
减少不必要的依赖传播****
晚上十点,苏雨又一次走到他工位前。
“我刚拉了最新代码。”她说,“体验……比我想象中好。”
林晨没有抬头:“但设置反馈比你原型里慢了大概 50ms。”
“普通用户感觉不到。”
“但你能。”他说。
苏雨笑了一下。
“你这个人,真的很前端。”
林晨也笑了。
他知道,这不是夸他写代码。
是认可他在边界里坚持。
深夜,PR 被合并。
陈浩在评论区只留了一句话:
“技术方案可解释,风险可控,先这样。”****
林晨关掉电脑,靠在椅子上。
他忽然意识到一件事——
真正的技术选型,从来不是框架、库、实现方式。
而是:
你愿不愿意为结果负责。