当用户打开页面后,等待的每一秒都在消磨耐心。卡顿的滚动、迟迟不出现的首屏内容,会直接推走用户。渲染性能的提升不是靠某个玄学技巧,而是围绕主线程优化、资源加载策略、渲染范围控制这几个角度的系统调整。下面分享一套可以直接落地到项目里的方法。
浏览器里最耗时的操作往往不是 JS 本身的运算,而是由 DOM 修改触发的布局计算和重绘。尤其是在循环里频繁插入节点,或者读写样式交错进行时,主线程会被迫反复进行"同步布局",这是卡顿的主要来源之一。
需要一次性创建大量 DOM 节点时,比如渲染一个长篇列表,可以考虑把节点先添加到 DocumentFragment 这个轻量的容器中,最后整体挂载到目标元素下。整个过程只触发一次页面重排,而不是每次插入节点都重新计算。类似地,如果业务场景允许,预先用字符串拼接好整块 HTML,再一次性设置到容器的 innerHTML 中,效果也优于循环中逐个插入。
代码里经常出现这种交错模式:先读取元素高度,紧接着修改样式,然后又读取宽度。这种操作会不断打断浏览器的渲染优化流程,强制同步计算布局。一个更稳妥的做法是把读取操作(比如获取 offsetTop、clientWidth)都收集到一个阶段,之后再集中执行写入操作。哪怕只是调整一下代码里的执行顺序,布局抖动的次数也能大幅下降。
一次性渲染几百上千条列表数据,会创建大量 DOM 节点,样式计算和内存占用随之飙升,滚动时卡顿几乎不可避免。虚拟列表的理念很简单:只渲染用户当前能看到的那些项目,用空白占位撑起滚动条的总高度。
列表项高度不一致时,可以在首次渲染后测量并缓存每一项的精确高度。快速滚动时,如果只渲染可见项,手指滑动过快容易看到空白区域。建议在视口上下各预留一个缓冲带(例如额外多渲染 5-10 项),给测量和渲染留出时间,避免白屏闪烁。
首屏加载速度很大程度取决于初期下载的 JavaScript 和执行时间。如果所有功能都打包进一个文件,用户还没访问某些功能,就得先把这些无关代码下载并解释一遍。资源拆分的思路是让首屏只做必需的事。
即使避免了上述明显问题,某些复杂请求也会因为计算量过大霸占主线程,造成用户操作停滞。在这里可以引入浏览器提供的工具来主动干预。
如果初始化代码和用户交互排队争抢主线程,交互往往会被延后。可以尝试把非关键的初始化任务用 requestIdleCallback 安排到浏览器空闲时段执行。在 React 这类框架环境里,也可以把耗时任务放到并发渲染的优先级较低一层,确保高优先级的用户事件处理不会被阻塞。
当某个函数一次性执行时间超过 50 毫秒,浏览器就会标记为长任务,这时页面已经出现可感知的卡顿。处理大批量数据时,可以把它切片分批执行,每一批之间调用 setTimeout 或调度 API,把主线程让出来给渲染和交互。分批处理还有一个额外好处,是可以随时给界面加一个真实的加载进度提示。
首先在开发者工具的 Performance 面板里录制一次加载过程,观察是从服务器返回 HTML 太慢,还是 JS 下载、执行阻塞了渲染。大多数情况下,白屏时间与首屏脚本解析体积呈正相关。按路由拆分代码,把非关键功能延迟加载,白屏时间通常能明显缩短。
虚拟列表解决了 DOM 数量的问题,但如果列表项内部的样式计算复杂(比如包含大阴影或复杂的渐变),即使是几十个节点也可能拖慢帧率。此外,监听 scroll 事件时如果直接执行较重逻辑,也会造成卡顿。检查一下事件处理函数里有没有频繁触发布局读取的操作,必要时用 requestAnimationFrame 或 passive 事件绑定来缓冲。
可以给图片预先设置一个占位容器或背景色,避免加载时突然撑开布局造成跳动。同时为图片设置合理的宽度和高度属性,让浏览器提前预留空间。快速下滑时,可以把懒加载的阈值调得更远一些(比如提前一个视口高度),让图片有更充裕的加载时间。
渲染性能优化不是一次性革命,而是一个持续观察和调整的过程。建议先从最明显的问题入手:合并 DOM 操作、疏解长列表、拆包首屏资源。之后打开开发者工具检查长任务和布局抖动,针对报告里高耗时项目逐一处理。每一次改动都建议做一次前后对比测量,用数据验证效果,而不是靠感觉判断。