页面不到一秒就出现了。用户点下按钮,什么都没发生。内容已经可见,网络请求也结束了,加载报告看上去体面得很。主线程只是太忙了——忙着解析、执行、注水 JavaScript,腾不出手来响应。 页面可以加载得很快,同时用起来很慢。如果你只优化首屏渲染,你衡量的可能只是界面变得可见的那一刻,而不是它变得可用的那一刻。 从一个真实交互开始 别一上来就定一个包体积目标。先从一个用户真的会做的动作开始:打开主导航、在搜索框里打字、应用一个筛选、展开商品面板、把商品加进购物车、提交表单。 然后记录用户实际经历的过程:点击,可见响应开始,界面更新完成。如果输入之后有一段明显的停顿,那这个页面就有响应性问题——哪怕初始内容出现得很快。 Interaction to Next Paint,也就是 INP,衡量的是整个页面访问过程中用户交互的延迟。Web 性能指南建议,在 75 分位上,INP 保持在 200 毫秒或更低,才算好的用户体验。现场数据是最有力的证据,因为它反映的是真实设备、网络和交互;实验室工具则用来复现和解释问题。 在 Performance 面板里找长任务 打开浏览器 DevTools,切到 Performance 面板,录制那个让你觉得慢的交互。录制过程中:等页面稳定下来,像用户那样点击或输入,界面更新后停止录制,然后检查交互附近的主线程时间线。 长任务指的是主 UI 线程连续忙碌至少 50 毫秒的一段不中断时间。在这段任务运行期间,浏览器无法处理另一个主线程任务,包括输入处理和渲染工作。 常见原因包括: - 开销大的事件处理器 - 解析和求值大型脚本 - 同步数据处理 - 重复渲染 - 强制布局 - 第三方脚本 - 注水工作 - 初始化还用不到的功能 在基于 Chromium 的 DevTools 里,长任务会在性能追踪中被打上视觉标记。展开这个任务,检查调用树或自下而上的视图,找出耗时的函数。别看到第一个大函数名就动手优化,先看完整调用栈——框架代码被执行,可能只是因为应用代码请求了太多工作。 衡量 JavaScript,而不只是传输体积 压缩可以让脚本在网络传输时显得很小,却给浏览器留下大量工作。JavaScript 仍然必须经历:下载、解压、解析、编译、执行。 一个只增加少量压缩负载的包,依然可能执行昂贵的初始化、遍历庞大的 DOM、注册大量监听器,或者安排重复工作。用 Network 面板了解传输体积,用 Performance 面板了解主线程开销。 可以问这几个问题: - 这个脚本在第一次交互之前是必需的吗? - 整个库都被用到了吗? - 初始化会不会为页面上并不存在的组件运行? - 每次导航时这个脚本都会执行吗? - 同一个计算是不是被重复执行? - 用户输入的那一刻,第三方代码在跑吗? 先删工作,再拆工作 代码分割有用,但最好的延迟 JavaScript,往往是页面不再需要的 JavaScript。找这些:没用到的库、重复的工具函数、过时的分析集成、超出支持浏览器策略的 polyfill、为了一个小助手引入的大依赖、全局加载却很少渲染的组件、可以用 HTML 或 CSS 替代的初始化。 举个例子,如果只有一条路由展示图表,就别在每个页面都加载图表库: const { renderChart } = await import("./chart.js"); renderChart(data); 动态导入把模块挪到它真正变得必要的那一刻之后。只有当这个功能确实不需要更早拿到代码时,它才改善响应性。把必需的工作直接挪进点击处理器,可能只是把加载延迟换成了交互延迟。更好的选择,可能是在空闲时间、或者主要内容变得可交互之后,再加载一个可选功能。 把长任务拆成更小的任务 有时候工作是必要的,但不必以一整段不中断的方式阻塞主线程。 想象一下处理一个大集合: function processAllItems(items) { for (const item of items) { processItem(item); } } 如果这个循环变成了长任务,就定期让出: async function processItems(items) { for (let index = 0; index < items.length; index++) { processItem(items[index]); if (index % 100 === 0) { await scheduler.yield(); } } } 让出会给浏览器一个机会,在继续之前先处理输入和渲染这类更高优先级的工作。 最佳的分块大小取决于每个条目的成本和目标设备。固定的条目数容易演示,但应该在真实应用里测量。 如果你的浏览器支持范围里没有 scheduler.yield(),就用合适的调度回退方案,并保持行为的渐进增强。 把 CPU 密集型工作移出主线程 对于不需要直接访问 DOM 的昂贵计算,可以考虑 Web Worker。 合适的候选包括: - 解析大型数据集 - 图像处理 - 压缩 - 复杂的搜索索引 - 数值计算 Worker 会增加消息传递和生命周期的复杂度。当测量显示 CPU 工作正在阻塞交互时才用它,而不是把它当作每个函数的默认包装。 让交互处理器保持专注 事件处理器应该只做产生下一个有用的视觉响应所需的最小工作。 这个处理器同步做了太多事: button.addEventListener("click", () => { calculateLargeReport(); storeAnalyticsPayload(); updateRecommendations(); openPanel(); }); 用户要等待无关的工作完成后,才能看到面板打开。

打开网易新闻 查看精彩图片